diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index f0182f806..e8d9f99f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -13,7 +13,7 @@ namespace Barotrauma private float? defaultZoom; public float DefaultZoom { - get { return defaultZoom ?? (GameMain.Config == null || GameMain.Config.EnableMouseLook ? 1.3f : 1.0f); } + get { return defaultZoom ?? (GameSettings.CurrentConfig.EnableMouseLook ? 1.3f : 1.0f); } set { defaultZoom = MathHelper.Clamp(value, 0.5f, 2.0f); @@ -269,10 +269,10 @@ namespace Barotrauma if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; } if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; } - if (GameMain.Config.KeyBind(InputType.Left).IsDown()) { moveInput.X -= 1.0f; } - if (GameMain.Config.KeyBind(InputType.Right).IsDown()) { moveInput.X += 1.0f; } - if (GameMain.Config.KeyBind(InputType.Down).IsDown()) { moveInput.Y -= 1.0f; } - if (GameMain.Config.KeyBind(InputType.Up).IsDown()) { moveInput.Y += 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Left].IsDown()) { moveInput.X -= 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Right].IsDown()) { moveInput.X += 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down].IsDown()) { moveInput.Y -= 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up].IsDown()) { moveInput.Y += 1.0f; } } velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); @@ -346,7 +346,7 @@ namespace Barotrauma float scaledZoom = MathHelper.Lerp(DefaultZoom, MinZoom, zoomOutAmount) * globalZoomScale; //zoom in further if zoomOutAmount is low and resolution is lower than reference float newZoom = scaledZoom * (MathHelper.Lerp(0.3f * (1f - Math.Min(globalZoomScale, 1f)), 0f, - (GameMain.Config == null || GameMain.Config.EnableMouseLook) ? (float)Math.Sqrt(offsetUnscaledLen) : 0.3f) + 1f); + (GameSettings.CurrentConfig.EnableMouseLook) ? (float)Math.Sqrt(offsetUnscaledLen) : 0.3f) + 1f); Zoom += (newZoom - zoom) / ZoomSmoothness; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index eacea79b3..9b31bf9bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -37,7 +37,8 @@ namespace Barotrauma targetPos = attackWorldPos; } targetPos.Y = -targetPos.Y; - GUI.DrawLine(spriteBatch, pos, targetPos, GUI.Style.Red * 0.5f, 0, 4); + + GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); if (wallTarget != null) { Vector2 wallTargetPos = wallTarget.Position; @@ -46,19 +47,19 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUI.Style.Red, Color.Black); - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUI.Style.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); } - /*GUI.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUI.Style.Red); - GUI.Font.DrawString(spriteBatch, "updatetargets: " + MathUtils.Round(updateTargetsTimer, 0.1f), pos - Vector2.UnitY * 100.0f, GUI.Style.Red); - GUI.Font.DrawString(spriteBatch, "cooldown: " + MathUtils.Round(coolDownTimer, 0.1f), pos - Vector2.UnitY * 120.0f, GUI.Style.Red);*/ + /*GUIStyle.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUIStyle.Red); + GUIStyle.Font.DrawString(spriteBatch, "updatetargets: " + MathUtils.Round(updateTargetsTimer, 0.1f), pos - Vector2.UnitY * 100.0f, GUIStyle.Red); + GUIStyle.Font.DrawString(spriteBatch, "cooldown: " + MathUtils.Round(coolDownTimer, 0.1f), pos - Vector2.UnitY * 120.0f, GUIStyle.Red);*/ Color stateColor = Color.White; switch (State) { case AIState.Attack: - stateColor = IsCoolDownRunning ? Color.Orange : GUI.Style.Red; + stateColor = IsCoolDownRunning ? Color.Orange : GUIStyle.Red; break; case AIState.Escape: stateColor = Color.LightBlue; @@ -78,13 +79,13 @@ namespace Barotrauma { GUI.DrawLine(spriteBatch, ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorA.X, -attachJoint.WorldAnchorA.Y)), - ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorB.X, -attachJoint.WorldAnchorB.Y)), GUI.Style.Green, 0, 4); + ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorB.X, -attachJoint.WorldAnchorB.Y)), GUIStyle.Green, 0, 4); } if (LatchOntoAI.AttachPos.HasValue) { GUI.DrawLine(spriteBatch, pos, - ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.AttachPos.Value.X, -LatchOntoAI.AttachPos.Value.Y)), GUI.Style.Green, 0, 3); + ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.AttachPos.Value.X, -LatchOntoAI.AttachPos.Value.Y)), GUIStyle.Green, 0, 3); } } @@ -108,12 +109,12 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y), new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y), - GUI.Style.Red * 0.5f, 0, 3); + GUIStyle.Red * 0.5f, 0, 3); - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, currentNode.ID.ToString(), new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), - GUI.Style.Red); + GUIStyle.Red); } } } @@ -124,7 +125,7 @@ namespace Barotrauma Vector2 hitPos = ConvertUnits.ToDisplayUnits(steeringManager.AvoidRayCastHitPosition); hitPos.Y = -hitPos.Y; - GUI.DrawLine(spriteBatch, hitPos, hitPos + new Vector2(steeringManager.AvoidDir.X, -steeringManager.AvoidDir.Y) * 100, GUI.Style.Red, width: 5); + GUI.DrawLine(spriteBatch, hitPos, hitPos + new Vector2(steeringManager.AvoidDir.X, -steeringManager.AvoidDir.Y) * 100, GUIStyle.Red, width: 5); //GUI.DrawLine(spriteBatch, pos, ConvertUnits.ToDisplayUnits(steeringManager.AvoidLookAheadPos.X, -steeringManager.AvoidLookAheadPos.Y), Color.Orange, width: 4); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 42ffa3a40..4be093613 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -18,7 +18,7 @@ namespace Barotrauma if (SelectedAiTarget?.Entity != null) { - //GUI.DrawLine(spriteBatch, pos, new Vector2(SelectedAiTarget.WorldPosition.X, -SelectedAiTarget.WorldPosition.Y), GUI.Style.Red); + //GUI.DrawLine(spriteBatch, pos, new Vector2(SelectedAiTarget.WorldPosition.X, -SelectedAiTarget.WorldPosition.Y), GUIStyle.Red); //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black); } @@ -87,7 +87,7 @@ namespace Barotrauma new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y), Color.Blue * 0.5f, 0, 3); - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, currentNode.ID.ToString(), new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), Color.Blue); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs index 0c21b2956..a1c712e7c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs @@ -6,16 +6,16 @@ namespace Barotrauma { public static Color ObjectiveIconColor => Color.LightGray; - public static Sprite GetSprite(string identifier, string option, Entity targetEntity) + public static Sprite GetSprite(Identifier identifier, Identifier option, Entity targetEntity) { - if (string.IsNullOrEmpty(identifier)) + if (identifier == Identifier.Empty) { return null; } - identifier = identifier.RemoveWhitespace(); - if (Order.Prefabs.TryGetValue(identifier, out Order orderPrefab)) + if (OrderPrefab.Prefabs.ContainsKey(identifier)) { - if (!string.IsNullOrEmpty(option) && orderPrefab.OptionSprites.TryGetValue(option, out var optionSprite)) + OrderPrefab orderPrefab = OrderPrefab.Prefabs[identifier]; + if (option != Identifier.Empty && orderPrefab.OptionSprites.TryGetValue(option, out var optionSprite)) { return optionSprite; } @@ -25,7 +25,7 @@ namespace Barotrauma } return orderPrefab.SymbolSprite; } - return GUI.Style.GetComponentStyle($"{identifier}objectiveicon")?.GetDefaultSprite(); + return GUIStyle.GetComponentStyle($"{identifier}objectiveicon")?.GetDefaultSprite(); } public Sprite GetSprite() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index f674cc079..6904554bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -28,13 +28,13 @@ namespace Barotrauma { if (item.Prefab.BrokenSprites.None()) { - Color c = item.prefab.SpriteColor; + Color c = item.Prefab.SpriteColor; item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } } foreach (var structure in thalamusStructures) { - Color c = structure.prefab.SpriteColor; + Color c = structure.Prefab.SpriteColor; structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } yield return CoroutineStatus.Running; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 9d3900b80..5e315ee8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -469,7 +469,7 @@ namespace Barotrauma Color? color = null; if (character.ExternalHighlight) { - color = Color.Lerp(Color.White, GUI.Style.Orange, (float)Math.Sin(Timing.TotalTime * 3.5f)); + color = Color.Lerp(Color.White, GUIStyle.Orange, (float)Math.Sin(Timing.TotalTime * 3.5f)); } float depthOffset = GetDepthOffset(); @@ -564,7 +564,7 @@ namespace Barotrauma Vector2 pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorB); if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; pos.Y = -pos.Y; - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)pos.Y, 5, 5), GUI.Style.Red, true, 0.01f); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)pos.Y, 5, 5), GUIStyle.Red, true, 0.01f); pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorA); if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; @@ -575,8 +575,8 @@ namespace Barotrauma limb.body.DebugDraw(spriteBatch, inWater ? (currentHull == null ? Color.Blue : Color.Cyan) : Color.White); } - Collider.DebugDraw(spriteBatch, frozen ? GUI.Style.Red : (inWater ? Color.SkyBlue : Color.Gray)); - GUI.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.FormatSingleDecimal(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange); + Collider.DebugDraw(spriteBatch, frozen ? GUIStyle.Red : (inWater ? Color.SkyBlue : Color.Gray)); + GUIStyle.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.FormatSingleDecimal(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange); foreach (var joint in LimbJoints) { @@ -607,10 +607,10 @@ namespace Barotrauma { Vector2 pos = ConvertUnits.ToDisplayUnits(humanoid.RightHandIKPos); if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUIStyle.Green, true); pos = ConvertUnits.ToDisplayUnits(humanoid.LeftHandIKPos); if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUIStyle.Green, true); Vector2 aimPos = humanoid.AimSourceWorldPos; aimPos.Y = -aimPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index 480a5fd26..2c7bc106e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -6,14 +6,14 @@ namespace Barotrauma { partial class Attack { - [Serialize("StructureBlunt", true), Editable()] + [Serialize("StructureBlunt", IsPropertySaveable.Yes), Editable()] public string StructureSoundType { get; private set; } private RoundSound sound; private ParticleEmitter particleEmitter; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { if (element.Attribute("sound") != null) { @@ -21,7 +21,7 @@ namespace Barotrauma return; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -29,7 +29,7 @@ namespace Barotrauma particleEmitter = new ParticleEmitter(subElement); break; case "sound": - sound = Submarine.LoadRoundSound(subElement); + sound = RoundSound.Load(subElement); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 89ca9a2f7..4bd56bce4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -131,7 +131,7 @@ namespace Barotrauma private class GUIMessage { public string RawText; - public string Identifier; + public Identifier Identifier; public string Text; private int _value; @@ -142,7 +142,7 @@ namespace Barotrauma { _value = value; Text = RawText.Replace("[value]", _value.ToString()); - Size = GUI.Font.MeasureString(Text); + Size = GUIStyle.Font.MeasureString(Text); } } @@ -154,7 +154,7 @@ namespace Barotrauma public bool PlaySound; - public GUIMessage(string rawText, Color color, float delay, string identifier = null, int? value = null, float lifeTime = 3.0f) + public GUIMessage(string rawText, Color color, float delay, Identifier identifier = default, int? value = null, float lifeTime = 3.0f) { RawText = Text = rawText; if (value.HasValue) @@ -163,7 +163,7 @@ namespace Barotrauma Value = value.Value; } Timer = -delay; - Size = GUI.Font.MeasureString(Text); + Size = GUIStyle.Font.MeasureString(Text); Color = color; Identifier = identifier; Lifetime = lifeTime; @@ -202,14 +202,14 @@ namespace Barotrauma get { return activeObjectiveEntities; } } - partial void InitProjSpecific(XElement mainElement) + partial void InitProjSpecific(ContentXElement mainElement) { soundTimer = Rand.Range(0.0f, Params.SoundInterval); sounds = new List(); Params.Sounds.ForEach(s => sounds.Add(new CharacterSound(s))); - foreach (XElement subElement in mainElement.Elements()) + foreach (var subElement in mainElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -267,11 +267,11 @@ namespace Barotrauma //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) { - if (GameMain.Config.KeyBind(InputType.Shoot).Equals(GameMain.Config.KeyBind(InputType.Select))) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Shoot] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select]) { keys[(int)InputType.Select].Reset(); } - if (GameMain.Config.KeyBind(InputType.Shoot).Equals(GameMain.Config.KeyBind(InputType.Use))) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Shoot] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use]) { keys[(int)InputType.Use].Reset(); } @@ -331,7 +331,7 @@ namespace Barotrauma Position + PlayerInput.MouseSpeed.ClampLength(10.0f); //apply a little bit of movement to the cursor pos to prevent AFK kicking } - else if (!GameMain.Config.EnableMouseLook) + else if (!GameSettings.CurrentConfig.EnableMouseLook) { cam.OffsetAmount = targetOffsetAmount = 0.0f; } @@ -446,15 +446,15 @@ namespace Barotrauma if (GameMain.NetworkMember != null && controlled == this) { - string chatMessage = CauseOfDeath.Type == CauseOfDeathType.Affliction ? + LocalizedString chatMessage = CauseOfDeath.Type == CauseOfDeathType.Affliction ? CauseOfDeath.Affliction.SelfCauseOfDeathDescription : - TextManager.Get("Self_CauseOfDeathDescription." + CauseOfDeath.Type.ToString(), fallBackTag: "Self_CauseOfDeathDescription.Damage"); + TextManager.Get("Self_CauseOfDeathDescription." + CauseOfDeath.Type.ToString(), "Self_CauseOfDeathDescription.Damage"); if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); } GameMain.NetworkMember.RespawnManager?.ShowRespawnPromptIfNeeded(); - GameMain.NetworkMember.AddChatMessage(chatMessage, ChatMessageType.Dead); + GameMain.NetworkMember.AddChatMessage(chatMessage.Value, ChatMessageType.Dead); GameMain.LightManager.LosEnabled = false; controlled = null; if (!(Screen.Selected?.Cam is null)) @@ -726,9 +726,9 @@ namespace Barotrauma } } - partial void SetOrderProjSpecific(Order order, string orderOption, int priority) + partial void SetOrderProjSpecific(Order order) { - GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order, orderOption, priority); + GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order); } public static void AddAllToGUIUpdateList() @@ -812,7 +812,7 @@ namespace Barotrauma Controlled != this && Submarine != null && Controlled.Submarine == Submarine && - GameMain.Config.LosMode != LosMode.None) + GameSettings.CurrentConfig.Graphics.LosMode != LosMode.None) { float yPos = Controlled.AnimController.FloorY - 1.5f; @@ -854,7 +854,7 @@ namespace Barotrauma if (speechBubbleTimer > 0.0f) { - GUI.SpeechBubbleIcon.Draw(spriteBatch, pos - Vector2.UnitY * 5, + GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, pos - Vector2.UnitY * 5, speechBubbleColor * Math.Min(speechBubbleTimer, 1.0f), 0.0f, Math.Min(speechBubbleTimer, 1.0f)); } @@ -880,7 +880,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, cursorPos, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), - ToolBox.GradientLerp(dist, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green), width: 2); + ToolBox.GradientLerp(dist, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green), width: 2); } } return; @@ -899,10 +899,10 @@ namespace Barotrauma { if (info != null) { - string name = Info.DisplayName; + LocalizedString name = Info.DisplayName; if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); } - Vector2 nameSize = GUI.Font.MeasureString(name); + Vector2 nameSize = GUIStyle.Font.MeasureString(name); Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; Color nameColor = GetNameColor(); @@ -916,7 +916,7 @@ namespace Barotrauma if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) { - var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; @@ -929,11 +929,11 @@ namespace Barotrauma } } - GUI.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); - GUI.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); + GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); + GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); if (GameMain.DebugDraw) { - GUI.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); + GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); } } @@ -941,7 +941,7 @@ namespace Barotrauma if (petBehavior != null && !IsDead && !IsUnconscious) { var petStatus = petBehavior.GetCurrentStatusIndicatorType(); - var iconStyle = GUI.Style.GetComponentStyle("PetIcon." + petStatus); + var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; @@ -963,7 +963,7 @@ namespace Barotrauma Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), CharacterHealth.DisplayedVitality / MaxVitality, - Color.Lerp(GUI.Style.Red, GUI.Style.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, + Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } } @@ -987,7 +987,7 @@ namespace Barotrauma } } - Color nameColor = GUI.Style.TextColor; + Color nameColor = GUIStyle.TextColorNormal; if (Controlled != null && team != Controlled.TeamID) { if (TeamID == CharacterTeamType.FriendlyNPC) @@ -996,13 +996,13 @@ namespace Barotrauma } else { - nameColor = GUI.Style.Red; + nameColor = GUIStyle.Red; } } return nameColor; } - public void AddMessage(string rawText, Color color, bool playSound, string identifier = null, int? value = null, float lifetime = 3.0f) + public void AddMessage(string rawText, Color color, bool playSound, Identifier identifier = default, int? value = null, float lifetime = 3.0f) { GUIMessage existingMessage = null; @@ -1089,12 +1089,12 @@ namespace Barotrauma matchingSounds.Clear(); foreach (var s in sounds) { - if (s.Type == soundType && (s.Gender == Gender.None || (info != null && info.Gender == s.Gender))) + if (s.Type == soundType && (s.TagSet.None() || (info != null && s.TagSet.IsSubsetOf(info.Head.Preset.TagSet)))) { matchingSounds.Add(s); } } - var selectedSound = matchingSounds.GetRandom(); + var selectedSound = matchingSounds.GetRandomUnsynced(); if (selectedSound?.Sound == null) { return; } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull, ignoreMuffling: selectedSound.IgnoreMuffling); soundTimer = Params.SoundInterval; @@ -1117,7 +1117,7 @@ namespace Barotrauma /// /// Note that when a predicate is provided, the random option uses Linq.Where() extension method, which creates a new collection. /// - public CharacterSound GetSound(Func predicate = null, bool random = false) => random ? sounds.GetRandom(predicate) : sounds.FirstOrDefault(predicate); + public CharacterSound GetSound(Func predicate = null, bool random = false) => random ? sounds.GetRandomUnsynced(predicate) : sounds.FirstOrDefault(predicate); partial void ImplodeFX() { @@ -1156,14 +1156,14 @@ namespace Barotrauma if (newAmount > prevAmount) { int increase = newAmount - prevAmount; - AddMessage("+" + TextManager.GetWithVariable("currencyformat", "[credits]", "[value]"), - GUI.Style.Yellow, playSound: this == Controlled, "money", increase); + AddMessage("+" + TextManager.GetWithVariable("currencyformat", "[credits]", "[value]").Value, + GUIStyle.Yellow, playSound: this == Controlled, "money".ToIdentifier(), increase); } } partial void OnTalentGiven(TalentPrefab talentPrefab) { - AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier), GUI.Style.Yellow, playSound: this == Controlled); + AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier).Value, GUIStyle.Yellow, playSound: this == Controlled); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 0455691f9..a911b5c70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -35,23 +35,23 @@ namespace Barotrauma MinSize = new Point(100, 50), RelativeOffset = new Vector2(0.0f, 0.01f) }, isHorizontal: false, childAnchor: Anchor.TopCenter); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUI.Style.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUIStyle.Red); TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) { MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y) }, barSize: 0.0f, style: "CharacterHealthBarCentered") { - Color = GUI.Style.Red + Color = GUIStyle.Red }; SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) { MinSize = new Point(80, 60) }, isHorizontal: false, childAnchor: Anchor.TopRight); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUI.Style.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") { - Color = GUI.Style.Red + Color = GUIStyle.Red }; TopContainer.Visible = SideContainer.Visible = false; @@ -72,7 +72,7 @@ namespace Barotrauma private static readonly List bossHealthBars = new List(); - private static readonly Dictionary cachedHudTexts = new Dictionary(); + private static readonly Dictionary cachedHudTexts = new Dictionary(); private static GUILayoutGroup bossHealthContainer; @@ -119,14 +119,12 @@ namespace Barotrauma !ConversationAction.FadeScreenToBlack; } - public static string GetCachedHudText(string textTag, string keyBind) + public static LocalizedString GetCachedHudText(string textTag, InputType keyBind) { - if (cachedHudTexts.TryGetValue(textTag + keyBind, out string text)) - { - return text; - } - text = TextManager.GetWithVariable(textTag, "[key]", keyBind); - cachedHudTexts.Add(textTag + keyBind, text); + Identifier key = (textTag + keyBind).ToIdentifier(); + if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } + text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; + cachedHudTexts.Add(key, text); return text; } @@ -256,24 +254,24 @@ namespace Barotrauma if (GameMain.GameSession?.CrewManager != null) { orderIndicatorCount.Clear(); - foreach (Pair activeOrder in GameMain.GameSession.CrewManager.ActiveOrders) + foreach (CrewManager.ActiveOrder activeOrder in GameMain.GameSession.CrewManager.ActiveOrders) { - if (!DrawIcon(activeOrder.First)) { continue; } + if (!DrawIcon(activeOrder.Order)) { continue; } - if (activeOrder.Second.HasValue) + if (activeOrder.FadeOutTime.HasValue) { - DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, iconAlpha: MathHelper.Clamp(activeOrder.Second.Value / 10.0f, 0.2f, 1.0f)); + DrawOrderIndicator(spriteBatch, cam, character, activeOrder.Order, iconAlpha: MathHelper.Clamp(activeOrder.FadeOutTime.Value / 10.0f, 0.2f, 1.0f)); } else { - float iconAlpha = GetDistanceBasedIconAlpha(activeOrder.First.TargetSpatialEntity, maxDistance: 450.0f); + float iconAlpha = GetDistanceBasedIconAlpha(activeOrder.Order.TargetSpatialEntity, maxDistance: 450.0f); if (iconAlpha <= 0.0f) { continue; } - DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, + DrawOrderIndicator(spriteBatch, cam, character, activeOrder.Order, iconAlpha: iconAlpha, createOffset: false, scaleMultiplier: 0.5f, overrideAlpha: true); } } - if (character.GetCurrentOrderWithTopPriority()?.Order is Order currentOrder && DrawIcon(currentOrder)) + if (character.GetCurrentOrderWithTopPriority() is Order currentOrder && DrawIcon(currentOrder)) { DrawOrderIndicator(spriteBatch, cam, character, currentOrder, 1.0f); } @@ -310,8 +308,8 @@ namespace Barotrauma if (!brokenItem.IsInteractable(character)) { continue; } float alpha = GetDistanceBasedIconAlpha(brokenItem); if (alpha <= 0.0f) continue; - GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUI.BrokenIcon, - Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); + GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite, + Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } float GetDistanceBasedIconAlpha(ISpatialEntity target, float maxDistance = 1000.0f) @@ -344,12 +342,12 @@ namespace Barotrauma circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); if (circleSize > 0.0f) { - Vector2 scale = new Vector2(circleSize / GUI.Style.FocusIndicator.FrameSize.X); - GUI.Style.FocusIndicator.Draw(spriteBatch, - (int)((focusedItemOverlayTimer - 1.0f) * GUI.Style.FocusIndicator.FrameCount * 3.0f), + Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X); + GUIStyle.FocusIndicator.Draw(spriteBatch, + (int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f), circlePos, Color.LightBlue * 0.3f, - origin: GUI.Style.FocusIndicator.FrameSize.ToVector2() / 2, + origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2, rotate: (float)Timing.TotalTime, scale: scale); } @@ -367,8 +365,8 @@ namespace Barotrauma int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); - Vector2 textSize = GUI.Font.MeasureString(hudTexts.First().Text); - Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(hudTexts.First().Text); + Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text); + Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text); Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition); startPos.Y -= (hudTexts.Count + 1) * textSize.Y; @@ -383,14 +381,14 @@ namespace Barotrauma float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f); - GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No); startPos.X += dir * 10.0f * GUI.Scale; textPos.X += dir * 10.0f * GUI.Scale; textPos.Y += largeTextSize.Y; foreach (ColoredText coloredText in hudTexts.Skip(1)) { - if (dir == -1) textPos.X = (int)(startPos.X - GUI.SmallFont.MeasureString(coloredText.Text).X); - GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUI.SmallFont); + if (dir == -1) textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X); + GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; } } @@ -405,7 +403,7 @@ namespace Barotrauma { if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } - var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); + var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); if (iconStyle == null) { continue; } Range visibleRange = new Range(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) @@ -491,7 +489,7 @@ namespace Barotrauma mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && !character.ShouldLockHud(); if (mouseOnPortrait) { - GUI.UIGlow.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea, GUI.Style.Green * 0.5f); + GUIStyle.UIGlow.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea, GUIStyle.Green * 0.5f); } } if (ShouldDrawInventory(character)) @@ -555,28 +553,28 @@ namespace Barotrauma string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; Vector2 textPos = startPos; - Vector2 textSize = GUI.Font.MeasureString(focusName); - Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(focusName); + Vector2 textSize = GUIStyle.Font.MeasureString(focusName); + Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(focusName); textPos -= new Vector2(textSize.X / 2, textSize.Y); Color nameColor = character.FocusedCharacter.GetNameColor(); - GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.X += 10.0f * GUI.Scale; - textPos.Y += GUI.SubHeadingFont.MeasureString(focusName).Y; + textPos.Y += GUIStyle.SubHeadingFont.MeasureString(focusName).Y; if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet) { - GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", GameMain.Config.KeyBindText(InputType.Use)), - GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", InputType.Use), + GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += largeTextSize.Y; } if (character.FocusedCharacter.CanBeDragged) { string text = character.CanEat ? "EatHint" : "GrabHint"; - GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, GameMain.Config.KeyBindText(InputType.Grab)), - GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, InputType.Grab), + GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += largeTextSize.Y; } @@ -585,13 +583,13 @@ namespace Barotrauma character.FocusedCharacter.CharacterHealth.UseHealthWindow && character.CanInteractWith(character.FocusedCharacter, 160f, false)) { - GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", GameMain.Config.KeyBindText(InputType.Health)), - GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health), + GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; } - if (!string.IsNullOrEmpty(character.FocusedCharacter.customInteractHUDText) && character.FocusedCharacter.AllowCustomInteract) + if (!character.FocusedCharacter.CustomInteractHUDText.IsNullOrEmpty() && character.FocusedCharacter.AllowCustomInteract) { - GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.customInteractHUDText, GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.CustomInteractHUDText, GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 025aadcd0..5d2358563 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Graphics; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Items.Components; +using System.Collections.Immutable; namespace Barotrauma { @@ -34,17 +35,17 @@ namespace Barotrauma public static void Init() { - infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); + infoAreaPortraitBG = GUIStyle.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } - partial void LoadHeadSpriteProjectSpecific(XElement limbElement) + partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement) { - XElement maskElement = limbElement.Element("tintmask"); + ContentXElement maskElement = limbElement.GetChildElement("tintmask"); if (maskElement != null) { - string tintMaskPath = maskElement.GetAttributeString("texture", ""); - if (!string.IsNullOrWhiteSpace(tintMaskPath)) + ContentPath tintMaskPath = maskElement.GetAttributeContentPath("texture"); + if (!tintMaskPath.IsNullOrEmpty()) { tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this)); tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f); @@ -66,7 +67,7 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect)); - ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; + GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font; var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { @@ -77,9 +78,9 @@ namespace Barotrauma Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } - GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUI.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUI.Font) + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero }; @@ -98,9 +99,11 @@ namespace Barotrauma }; } - if (personalityTrait != null) + if (PersonalityTrait != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + personalityTrait.Name.Replace(" ", ""))), font: font) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), + TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + PersonalityTrait.Name.Replace(" ".ToIdentifier(), "".ToIdentifier()))), + font: font) { Padding = Vector4.Zero }; @@ -148,10 +151,10 @@ namespace Barotrauma Stretch = true }; - string deadDescription = TextManager.AddPunctuation(':', TextManager.Get("deceased") + "\n" + Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? + LocalizedString deadDescription = TextManager.AddPunctuation(':', TextManager.Get("deceased") + "\n" + Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + Character.CauseOfDeath.Type.ToString()))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), deadArea.RectTransform), deadDescription, textColor: GUI.Style.Red, font: font, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + 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) @@ -182,13 +185,13 @@ namespace Barotrauma 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: GUI.SmallFont); + 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(string skillIdentifier, float prevLevel, float newLevel) + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } @@ -199,9 +202,10 @@ namespace Barotrauma if ((int)newLevel > (int)prevLevel) { int increase = Math.Max((int)newLevel - (int)prevLevel, 1); + Character?.AddMessage( - "+[value] "+ TextManager.Get("SkillName." + skillIdentifier), - specialIncrease ? GUI.Style.Orange : GUI.Style.Green, + "+[value] "+ TextManager.Get("SkillName." + skillIdentifier).Value, + specialIncrease ? GUIStyle.Orange : GUIStyle.Green, playSound: Character == Character.Controlled, skillIdentifier, increase); } } @@ -216,8 +220,8 @@ namespace Barotrauma { int increase = newAmount - prevAmount; Character?.AddMessage( - "+[value] " + TextManager.Get("experienceshort"), - GUI.Style.Blue, playSound: Character == Character.Controlled, "exp", increase); + "+[value] " + TextManager.Get("experienceshort").Value, + GUIStyle.Blue, playSound: Character == Character.Controlled, "exp".ToIdentifier(), increase); } } @@ -227,9 +231,13 @@ namespace Barotrauma if (idCard.StoredOwnerAppearance.JobPrefab == null || idCard.StoredOwnerAppearance.Portrait == null) { - string[] readTags = idCard.Item.Tags.Split(','); + var readTags = idCard.Item.Tags.Split(',') + .Where(s => s.Contains(':')) + .Select(s => s.Split(':')) + .Select(s => (s[0].ToIdentifier(),s[1])) + .ToImmutableDictionary(); - if (readTags.Length == 0) { return; } + if (readTags.None()) { return; } if (idCard.StoredOwnerAppearance.JobPrefab == null) { @@ -238,7 +246,7 @@ namespace Barotrauma if (idCard.StoredOwnerAppearance.Portrait == null) { - idCard.StoredOwnerAppearance.ExtractAppearance(this, readTags); + idCard.StoredOwnerAppearance.ExtractAppearance(this, idCard); } } @@ -267,17 +275,17 @@ namespace Barotrauma { LoadHeadAttachments(); } - FaceAttachment?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.FaceAttachment))); - BeardElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Beard))); - MoustacheElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Moustache))); - HairElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Hair))); + 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))); if (omitJob) { - JobPrefab.NoJobElement?.Element("PortraitClothing")?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); + JobPrefab.NoJobElement?.GetChildElement("PortraitClothing")?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); } else { - Job?.Prefab.ClothingElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); + Job?.Prefab.ClothingElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); } } @@ -288,7 +296,7 @@ namespace Barotrauma { if (sprite == null) { return; } if (Head.SheetIndex == null) { return; } - Point location = CalculateOffset(sprite, Head.SheetIndex.Value.ToPoint()); + Point location = CalculateOffset(sprite, Head.SheetIndex.ToPoint()); sprite.SourceRect = new Rectangle(location, sprite.SourceRect.Size); } @@ -414,19 +422,16 @@ namespace Barotrauma { var currEffect = spriteBatch.GetCurrentEffect(); float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); - if (Head.SheetIndex.HasValue) - { - headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.Value.ToPoint()), headSprite.SourceRect.Size); - } + headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.ToPoint()), headSprite.SourceRect.Size); SetHeadEffect(spriteBatch); - headSprite.Draw(spriteBatch, screenPos, scale: scale, color: SkinColor); + headSprite.Draw(spriteBatch, screenPos, scale: scale, color: Head.SkinColor); 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, HairColor, FacialHairColor)); + DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, Head.HairColor, Head.FacialHairColor)); depthStep += depthStep; } } @@ -479,14 +484,17 @@ namespace Barotrauma attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } - public static CharacterInfo ClientRead(string speciesName, IReadMessage inc) + public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); string originalName = inc.ReadString(); - int gender = inc.ReadByte(); - int race = inc.ReadByte(); - int headSpriteID = inc.ReadByte(); + 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(); @@ -500,14 +508,14 @@ namespace Barotrauma int variant = inc.ReadByte(); JobPrefab jobPrefab = null; - Dictionary skillLevels = new Dictionary(); + Dictionary skillLevels = new Dictionary(); if (!string.IsNullOrEmpty(jobIdentifier)) { jobPrefab = JobPrefab.Get(jobIdentifier); byte skillCount = inc.ReadByte(); for (int i = 0; i < skillCount; i++) { - string skillIdentifier = inc.ReadString(); + Identifier skillIdentifier = inc.ReadIdentifier(); float skillLevel = inc.ReadSingle(); skillLevels.Add(skillIdentifier, skillLevel); } @@ -518,14 +526,14 @@ namespace Barotrauma { ID = infoID, }; - ch.RecreateHead(headSpriteID,(Race)race, (Gender)gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); - ch.SkinColor = skinColor; - ch.HairColor = hairColor; - ch.FacialHairColor = facialHairColor; + ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + ch.Head.SkinColor = skinColor; + ch.Head.HairColor = hairColor; + ch.Head.FacialHairColor = facialHairColor; ch.SetPersonalityTrait(); if (ch.Job != null) { - foreach (KeyValuePair skill in skillLevels) + foreach (KeyValuePair skill in skillLevels) { Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key); if (matchingSkill == null) @@ -538,17 +546,8 @@ namespace Barotrauma ch.Job.Skills.RemoveAll(s => !skillLevels.ContainsKey(s.Identifier)); } - byte savedStatValueCount = inc.ReadByte(); - for (int i = 0; i < savedStatValueCount; i++) - { - int statType = inc.ReadByte(); - string statIdentifier = inc.ReadString(); - float statValue = inc.ReadSingle(); - bool removeOnDeath = inc.ReadBoolean(); - ch.ChangeSavedStatValue((StatTypes)statType, statValue, statIdentifier, removeOnDeath); - } ch.ExperiencePoints = inc.ReadUInt16(); - ch.AdditionalTalentPoints = inc.ReadUInt16(); + ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); return ch; } @@ -605,12 +604,12 @@ namespace Barotrauma { RelativeOffset = new Vector2(-0.01f, 0.0f) }); } - RectTransform createItemRectTransform(string labelTag, float width = 0.6f) + 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: GUI.SubHeadingFont); + TextManager.Get(labelTag), font: GUIStyle.SubHeadingFont); var bottomItem = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), style: null); @@ -618,43 +617,40 @@ namespace Barotrauma return new RectTransform(new Vector2(width, 1.0f), bottomItem.RectTransform, Anchor.Center); } - RectTransform genderItemRT = createItemRectTransform("Gender", 1.0f); + RectTransform menuCategoryRT = createItemRectTransform(info.Prefab.MenuCategoryVar, 1.0f); - GUILayoutGroup genderContainer = - new GUILayoutGroup(genderItemRT, isHorizontal: true) + GUILayoutGroup menuCategoryContainer = + new GUILayoutGroup(menuCategoryRT, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; - void createGenderButton(Gender gender) + void createMenuCategoryButton(Identifier tag) { - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), - TextManager.Get(gender.ToString()), style: "ListBoxElement") + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), menuCategoryContainer.RectTransform), + TextManager.Get(tag), style: "ListBoxElement") { - UserData = gender, + UserData = tag, OnClicked = OpenHeadSelection, - Selected = info.Gender == gender + Selected = info.Head.Preset.TagSet.Contains(tag) }; } - createGenderButton(Gender.Male); - createGenderButton(Gender.Female); - - int countAttachmentsOfType(WearableType wearableType) - => info.FilterByTypeAndHeadID( - info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), - wearableType, info.HeadSpriteId).Count(); + 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 = countAttachmentsOfType(wearableType); + int attachmentCount = info.CountValidAttachmentsOfType(wearableType); if (attachmentCount > 0) { var labelTag = wearableType == WearableType.FaceAttachment - ? "FaceAttachment.Accessories" - : $"FaceAttachment.{wearableType}"; + ? "FaceAttachment.Accessories".ToIdentifier() + : $"FaceAttachment.{wearableType}".ToIdentifier(); var sliderItemRT = createItemRectTransform(labelTag); var slider = new GUIScrollBar(sliderItemRT, style: "GUISlider") @@ -670,12 +666,12 @@ namespace Barotrauma } } - createAttachmentSlider(info.HairIndex, WearableType.Hair); - createAttachmentSlider(info.BeardIndex, WearableType.Beard); - createAttachmentSlider(info.MoustacheIndex, WearableType.Moustache); - createAttachmentSlider(info.FaceAttachmentIndex, WearableType.FaceAttachment); + 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(string labelTag, IEnumerable<(Color Color, float Commonness)> options, Func getter, + void createColorSelector(Identifier labelTag, IEnumerable<(Color Color, float Commonness)> options, Func getter, Action setter) { var selectorItemRT = createItemRectTransform(labelTag, 0.4f); @@ -757,21 +753,21 @@ namespace Barotrauma }; } - if (countAttachmentsOfType(WearableType.Hair) > 0) + if (info.CountValidAttachmentsOfType(WearableType.Hair) > 0) { - createColorSelector($"Customization.{nameof(info.HairColor)}", info.HairColors, - () => info.HairColor, (color) => info.HairColor = color); + createColorSelector($"Customization.{nameof(info.Head.HairColor)}".ToIdentifier(), info.HairColors, + () => info.Head.HairColor, (color) => info.Head.HairColor = color); } - if (countAttachmentsOfType(WearableType.Moustache) > 0 || - countAttachmentsOfType(WearableType.Beard) > 0) + if (info.CountValidAttachmentsOfType(WearableType.Moustache) > 0 || + info.CountValidAttachmentsOfType(WearableType.Beard) > 0) { - createColorSelector($"Customization.{nameof(info.FacialHairColor)}", info.FacialHairColors, - () => info.FacialHairColor, (color) => info.FacialHairColor = color); + createColorSelector($"Customization.{nameof(info.Head.FacialHairColor)}".ToIdentifier(), info.FacialHairColors, + () => info.Head.FacialHairColor, (color) => info.Head.FacialHairColor = color); } - - createColorSelector($"Customization.{nameof(info.SkinColor)}", info.SkinColors, () => info.SkinColor, - (color) => info.SkinColor = color); + + createColorSelector($"Customization.{nameof(info.Head.SkinColor)}".ToIdentifier(), info.SkinColors, () => info.Head.SkinColor, + (color) => info.Head.SkinColor = color); RandomizeButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, parentComponent.RectTransform, @@ -780,10 +776,11 @@ namespace Barotrauma { OnClicked = (button, o) => { - info.Head = new HeadInfo(); - info.SetGenderAndRace(Rand.RandSync.Unsynced); - info.SetColors(); - + 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); @@ -801,7 +798,7 @@ namespace Barotrauma private bool OpenHeadSelection(GUIButton button, object userData) { - Gender selectedGender = (Gender)userData; + Identifier selectedCategory = (Identifier)userData; var info = CharacterInfo; @@ -842,36 +839,27 @@ namespace Barotrauma GUILayoutGroup row = null; int itemsInRow = 0; - XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => + ContentXElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); - XElement headSpriteElement = headElement.Element("sprite"); + ContentXElement headSpriteElement = headElement.GetChildElement("sprite"); string spritePathWithTags = headSpriteElement.Attribute("texture").Value; var characterConfigElement = info.CharacterConfigElement; - var heads = info.Heads; + var heads = info.Prefab.Heads; if (heads != null) { row = null; itemsInRow = 0; - foreach (var kvp in heads.Where(kv => kv.Key.Gender == selectedGender)) + foreach (var head in heads.Where(h => h.TagSet.Contains(selectedCategory))) { - var headPreset = kvp.Key; - Race race = headPreset.Race; - int headIndex = headPreset.ID; + string spritePath = info.Prefab.ReplaceVars(spritePathWithTags, head); - string spritePath = spritePathWithTags - .Replace("[GENDER]", selectedGender.ToString().ToLowerInvariant()) - .Replace("[RACE]", race.ToString().ToLowerInvariant()); - - if (!File.Exists(spritePath)) - { - continue; - } + if (!File.Exists(spritePath)) { continue; } Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); headSprite.SourceRect = - new Rectangle(CalculateOffset(headSprite, kvp.Value.ToPoint()), + new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.SheetIndex.ToPoint()), headSprite.SourceRect.Size); characterSprites.Add(headSprite); @@ -881,7 +869,7 @@ namespace Barotrauma new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), true) { - UserData = selectedGender, + UserData = head.MenuCategory, Visible = true }; itemsInRow = 0; @@ -892,9 +880,9 @@ namespace Barotrauma { OutlineColor = Color.White * 0.5f, PressedColor = Color.White * 0.5f, - UserData = new Tuple(selectedGender, race, headIndex), + UserData = head, OnClicked = SwitchHead, - Selected = selectedGender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, + Selected = info.Head.Preset == head, Visible = true }; @@ -909,12 +897,18 @@ namespace Barotrauma private bool SwitchHead(GUIButton button, object obj) { var info = CharacterInfo; - Gender gender = ((Tuple)obj).Item1; - Race race = ((Tuple)obj).Item2; - int id = ((Tuple)obj).Item3; - info.Gender = gender; - info.Race = race; - info.Head.HeadSpriteId = id; + var headPreset = obj as HeadPreset; + if (info.Head.Preset != headPreset) + { + info.Head = new HeadInfo(info, headPreset) + { + SkinColor = info.Head.SkinColor, + HairColor = info.Head.HairColor, + FacialHairColor = info.Head.FacialHairColor + }; + info.ReloadHeadAttachments(); + } + RecreateFrameContents(); OnHeadSwitch?.Invoke(this); return true; @@ -927,16 +921,16 @@ namespace Barotrauma switch (type) { case WearableType.Beard: - info.BeardIndex = index; + info.Head.BeardIndex = index; break; case WearableType.FaceAttachment: - info.FaceAttachmentIndex = index; + info.Head.FaceAttachmentIndex = index; break; case WearableType.Hair: - info.HairIndex = index; + info.Head.HairIndex = index; break; case WearableType.Moustache: - info.MoustacheIndex = index; + info.Head.MoustacheIndex = index; break; default: DebugConsole.ThrowError($"Wearable type not implemented: {type}"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 946301454..a3dba23b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -134,7 +134,7 @@ namespace Barotrauma msg.Write((ushort)characterTalents.Count); foreach (var unlockedTalent in characterTalents) { - msg.Write(unlockedTalent.Prefab.UIntIdentifier); + msg.Write(unlockedTalent.Prefab.UintIdentifier); } break; } @@ -307,7 +307,7 @@ namespace Barotrauma { string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")"; DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); //read anyway to prevent messing up reading the rest of the message _ = msg.ReadUInt16(); @@ -357,7 +357,7 @@ namespace Barotrauma int skillCount = msg.ReadByte(); for (int i = 0; i < skillCount; i++) { - string skillIdentifier = msg.ReadString(); + Identifier skillIdentifier = msg.ReadIdentifier(); float skillLevel = msg.ReadSingle(); info?.SetSkillLevel(skillIdentifier, skillLevel); } @@ -419,9 +419,9 @@ namespace Barotrauma if (!validData) { break; } if (msgType == 1) { - int orderIndex = msg.ReadRangedInteger(0, Order.PrefabList.Count); - var orderPrefab = Order.PrefabList[orderIndex]; - string option = null; + UInt32 orderPrefabUintIdentifier = msg.ReadUInt32(); + var orderPrefab = OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); + Identifier option = Identifier.Empty; if (orderPrefab.HasOptions) { int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length); @@ -434,8 +434,8 @@ namespace Barotrauma } else if (msgType == 2) { - string identifier = msg.ReadString(); - string option = msg.ReadString(); + Identifier identifier = msg.ReadIdentifier(); + Identifier option = msg.ReadIdentifier(); ushort objectiveTargetEntityId = msg.ReadUInt16(); var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId); GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity); @@ -486,8 +486,8 @@ namespace Barotrauma break; case 13: //NetEntityEvent.Type.UpdatePermanentStats: byte savedStatValueCount = msg.ReadByte(); - StatTypes statType = (StatTypes)msg.ReadByte(); - info?.ClearSavedStatValues(statType); + StatTypes statType = (StatTypes)msg.ReadByte(); + info?.ClearSavedStatValues(statType); for (int i = 0; i < savedStatValueCount; i++) { string statIdentifier = msg.ReadString(); @@ -544,7 +544,7 @@ namespace Barotrauma int ownerId = hasOwner ? inc.ReadByte() : -1; byte teamID = inc.ReadByte(); bool hasAi = inc.ReadBoolean(); - string infoSpeciesName = inc.ReadString(); + Identifier infoSpeciesName = inc.ReadIdentifier(); CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); try @@ -567,7 +567,7 @@ namespace Barotrauma int orderCount = inc.ReadByte(); for (int i = 0; i < orderCount; i++) { - int orderPrefabIndex = inc.ReadByte(); + UInt32 orderPrefabUintIdentifier = inc.ReadUInt32(); Entity targetEntity = FindEntityByID(inc.ReadUInt16()); Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null; int orderOptionIndex = inc.ReadByte(); @@ -581,18 +581,23 @@ namespace Barotrauma targetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true); } - if (orderPrefabIndex >= 0 && orderPrefabIndex < Order.PrefabList.Count) + OrderPrefab orderPrefab = + OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); + if (orderPrefab != null) { - var orderPrefab = Order.PrefabList[orderPrefabIndex]; var component = orderPrefab.GetTargetItemComponent(targetEntity as Item); if (!orderPrefab.MustSetTarget || (targetEntity != null && component != null) || targetPosition != null) { var order = targetPosition == null ? new Order(orderPrefab, targetEntity, component, orderGiver: orderGiver) : new Order(orderPrefab, targetPosition, orderGiver: orderGiver); - character.SetOrder(order, - orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, - orderPriority, orderGiver, speak: false, force: true); + order = order.WithOption( + orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length + ? orderPrefab.Options[orderOptionIndex] + : Identifier.Empty) + .WithManualPriority(orderPriority) + .WithOrderGiver(orderGiver); + character.SetOrder(order, speak: false, force: true); } else { @@ -601,7 +606,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Invalid order prefab index - index (" + orderPrefabIndex + ") out of bounds."); + DebugConsole.ThrowError("Invalid order prefab index - index (" + orderPrefabUintIdentifier + ") out of bounds."); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs index 3cea60b80..5f3574971 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs @@ -1,4 +1,5 @@ using Barotrauma.Sounds; +using System.Collections.Immutable; namespace Barotrauma { @@ -13,20 +14,17 @@ namespace Barotrauma public readonly CharacterParams.SoundParams Params; public SoundType Type => Params.State; - public Gender Gender => Params.Gender; + public ImmutableHashSet TagSet => Params.TagSet; public float Volume => roundSound == null ? 0.0f : roundSound.Volume; public float Range => roundSound == null ? 0.0f : roundSound.Range; public Sound Sound => roundSound?.Sound; - public bool IgnoreMuffling - { - get { return roundSound?.IgnoreMuffling ?? false; } - } + public bool IgnoreMuffling => roundSound?.IgnoreMuffling ?? false; public CharacterSound(CharacterParams.SoundParams soundParams) { Params = soundParams; - roundSound = Submarine.LoadRoundSound(soundParams.Element); + roundSound = RoundSound.Load(soundParams.Element); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs index 5052e74fe..7738ca9ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs @@ -39,7 +39,7 @@ namespace Barotrauma public Vector2 Size; private readonly Submarine parentSub; - public string Text + public LocalizedString Text { get; private set; @@ -58,7 +58,7 @@ namespace Barotrauma } public HUDProgressBar(Vector2 worldPosition, string textTag, Submarine parentSubmarine = null) - : this(worldPosition, parentSubmarine, GUI.Style.Red, GUI.Style.Green, textTag) + : this(worldPosition, parentSubmarine, GUIStyle.Red, GUIStyle.Green, textTag) { } @@ -73,7 +73,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(textTag)) { this.textTag = textTag; - Text = TextManager.Get(textTag); + Text = TextManager.Get(textTag).Fallback(textTag); } } @@ -101,12 +101,12 @@ namespace Barotrauma color * a, Color.White * a * 0.8f); - if (!string.IsNullOrEmpty(Text)) + if (!Text.IsNullOrEmpty()) { - Vector2 textSize = GUI.SmallFont.MeasureString(Text); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(Text); Vector2 textPos = new Vector2(pos.X + (Size.X - textSize.X) / 2, pos.Y - textSize.Y * 1.2f); - GUI.DrawString(spriteBatch, textPos - Vector2.One, Text, Color.Black * a, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, textPos, Text, Color.White * a, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos - Vector2.One, Text, Color.Black * a, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, textPos, Text, Color.White * a, font: GUIStyle.SmallFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index e5bd89ebb..f89b83bdf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -16,15 +16,15 @@ { return; } - GUI.AddMessage(TextManager.Get("HuskDormant"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); break; case InfectionState.Transition: - GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); break; case InfectionState.Active: if (character.Params.UseHuskAppendage) { - GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameMain.Config.KeyBindText(InputType.Attack)), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red); } break; case InfectionState.Final: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index 20249f49b..77672ac7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -75,7 +75,7 @@ namespace Barotrauma case FloodType.Minor: currentFloodState += deltaTime; //lerp the water surface in all hulls 15 units above the floor within 10 seconds - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = hull.FakeFireSources.Count - 1; i >= 0; i--) { @@ -87,7 +87,7 @@ namespace Barotrauma case FloodType.Major: currentFloodState += deltaTime; //create a full flood in 10 seconds - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = hull.FakeFireSources.Count - 1; i >= 0; i--) { @@ -98,7 +98,7 @@ namespace Barotrauma break; case FloodType.HideFlooding: //hide water inside hulls (the player can't see which hulls are flooded) - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.DrawSurface = hull.Rect.Y - hull.Rect.Height; } @@ -140,7 +140,7 @@ namespace Barotrauma character.Submarine != null && createFireSourceTimer > MathHelper.Lerp(MaxFakeFireSourceInterval, MinFakeFireSourceInterval, Strength / 100.0f)) { - Hull fireHull = Hull.hullList.GetRandom(h => h.Submarine == character.Submarine); + Hull fireHull = Hull.HullList.GetRandomUnsynced(h => h.Submarine == character.Submarine); if (fireHull != null) { var fakeFire = new DummyFireSource(Vector2.One * 500.0f, new Vector2(Rand.Range(fireHull.WorldRect.X, fireHull.WorldRect.Right), fireHull.WorldPosition.Y + 1), fireHull, isNetworkMessage: true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d3773294f..c84fe8eab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -14,11 +14,31 @@ namespace Barotrauma { private static bool toggledThisFrame; - public static Sprite DamageOverlay; + public class DamageOverlayPrefab : Prefab + { + public readonly static PrefabSelector Prefabs = new PrefabSelector(); - public static string DamageOverlayFile; + public readonly Sprite DamageOverlay; - private static string[] strengthTexts; + public DamageOverlayPrefab(ContentXElement element, AfflictionsFile file) : base(file, file.Path.Value.ToIdentifier()) + { + DamageOverlay = new Sprite(element); + } + + public override void Dispose() + { + DamageOverlay.Remove(); + } + } + + public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay; + + private readonly static LocalizedString[] strengthTexts = new LocalizedString[] + { + TextManager.Get("AfflictionStrengthLow"), + TextManager.Get("AfflictionStrengthMedium"), + TextManager.Get("AfflictionStrengthHigh") + }; private Point screenResolution; @@ -134,7 +154,7 @@ namespace Barotrauma Character.Controlled.ResetInteract = true; if (openHealthWindow != null) { - if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner")) + if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier())) { openHealthWindow.characterName.Text = value.Character.Name; } @@ -173,20 +193,10 @@ namespace Barotrauma private GUIFrame healthBarHolder; - partial void InitProjSpecific(XElement element, Character character) + partial void InitProjSpecific(ContentXElement element, Character character) { DisplayedVitality = MaxVitality; - if (strengthTexts == null) - { - strengthTexts = new string[] - { - TextManager.Get("AfflictionStrengthLow"), - TextManager.Get("AfflictionStrengthMedium"), - TextManager.Get("AfflictionStrengthHigh") - }; - } - character.OnAttacked += OnAttacked; healthWindow = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.6f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIFrameListBox"); @@ -200,13 +210,13 @@ namespace Barotrauma { Stretch = true }; - + new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) => { character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, character != Character.Controlled); }); - characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) + characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; @@ -220,12 +230,12 @@ namespace Barotrauma var healthBarContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), healthWindowVerticalLayout.RectTransform), style: null); var healthBarIcon = new GUIFrame(new RectTransform(new Vector2(0.095f, 1.0f), healthBarContainer.RectTransform), style: "GUIHealthBarIcon"); healthWindowHealthBarShadow = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + barSize: 1.0f, color: GUIStyle.Green, style: "GUIHealthBar") { IsHorizontal = true }; healthWindowHealthBar = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + barSize: 1.0f, color: GUIStyle.Green, style: "GUIHealthBar") { IsHorizontal = true }; @@ -311,7 +321,7 @@ namespace Barotrauma } ); deadIndicator = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.1f), limbSelection.RectTransform, Anchor.Center), - text: TextManager.Get("Deceased"), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "GUIToolTip") + text: TextManager.Get("Deceased"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: "GUIToolTip") { Visible = false, CanBeFocused = false @@ -328,7 +338,7 @@ namespace Barotrauma afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), - TextManager.Get("SuitableTreatments"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomCenter); + TextManager.Get("SuitableTreatments"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); treatmentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), healthWindowVerticalLayout.RectTransform), true) { @@ -366,10 +376,10 @@ namespace Barotrauma healthShadowSize = 1.0f; healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: "CharacterHealthBar") + barSize: 1.0f, color: GUIStyle.HealthBarColorHigh, style: "CharacterHealthBar") { HoverCursor = CursorState.Hand, - ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameMain.Config.KeyBindText(InputType.Health)), + ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)), Enabled = true }; @@ -406,7 +416,7 @@ namespace Barotrauma if (element != null) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -501,7 +511,17 @@ namespace Barotrauma { if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && Character.Controlled == Character) { - SoundPlayer.PlaySound(Character.Info != null && Character.Info.Gender == Gender.Female ? "drownfemale" : "drownmale"); + string soundName; + if (Character.Info != null) + { + soundName = Character.Info.ReplaceVars($"drown[{Character.Info.Prefab.MenuCategoryVar}]"); + } + else + { + var charInfoPrefab = CharacterPrefab.HumanPrefab.CharacterInfoPrefab; + soundName = charInfoPrefab.ReplaceVars($"drown[{charInfoPrefab.MenuCategoryVar}]", charInfoPrefab.Heads.First()); + } + SoundPlayer.PlaySound(soundName); } if (Character == Character.Controlled && !IsUnconscious && !Character.IsDead && OxygenAmount < LowOxygenThreshold) @@ -715,11 +735,11 @@ namespace Barotrauma if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { - afflictionIcon.Flash(GUI.Style.Red); + afflictionIcon.Flash(GUIStyle.Red); } else if (affliction.AppliedAsSuccessfulTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { - afflictionIcon.Flash(GUI.Style.Green); + afflictionIcon.Flash(GUIStyle.Green); } } @@ -754,7 +774,7 @@ namespace Barotrauma var labelContainer = afflictionTooltip.Content.GetChildByUserData("label"); - labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, (int)(GUI.LargeFont.Size * 1.5f))); + labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f))); } } else @@ -799,7 +819,7 @@ namespace Barotrauma { var treatmentButton = component.GetChild(); if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; } - var matchingItem = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); + var matchingItem = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); treatmentButton.Enabled = matchingItem != null; if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) { @@ -809,16 +829,18 @@ namespace Barotrauma if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length && Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f) { - Character.Controlled.Inventory.visualSlots[index].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f); + Character.Controlled.Inventory.visualSlots[index].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f); } } - if (matchingItem != null && !string.IsNullOrEmpty(treatmentButton.ToolTip)) { continue; } - treatmentButton.ToolTip = $"‖color:255,255,255,255‖{itemPrefab.Name}‖color:end‖" + '\n' + itemPrefab.Description; + if (matchingItem != null && !treatmentButton.ToolTip.IsNullOrEmpty()) { continue; } + treatmentButton.ToolTip = RichString.Rich($"‖color:255,255,255,255‖{itemPrefab.Name}‖color:end‖" + '\n' + itemPrefab.Description); if (treatmentButton.Enabled) { treatmentButton.ToolTip = - $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] {TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' - + treatmentButton.RawToolTip; + RichString.Rich( + $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] " + + $"{TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + + treatmentButton.ToolTip.NestedStr); } foreach (GUIComponent child in treatmentButton.Children) { @@ -834,7 +856,7 @@ namespace Barotrauma } else { - healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUI.Style.HealthBarColorLow, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorHigh); + healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUIStyle.HealthBarColorLow, GUIStyle.HealthBarColorMedium, GUIStyle.HealthBarColorHigh); healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; healthBar.BarSize = healthWindowHealthBar.BarSize = (DisplayedVitality > 0.0f) ? @@ -1007,14 +1029,14 @@ namespace Barotrauma DrawStatusHUD(spriteBatch); } - private (Affliction affliction, string text)? highlightedAfflictionIcon; + private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; public void DrawStatusHUD(SpriteBatch spriteBatch) { highlightedAfflictionIcon = null; //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { - List<(Affliction affliction, string text)> statusIcons = new List<(Affliction affliction, string text)>(); + var statusIcons = new List<(Affliction Affliction, LocalizedString Warning)>(); if (Character.InPressure) { statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); @@ -1045,7 +1067,7 @@ namespace Barotrauma foreach (var statusIcon in statusIcons) { - Affliction affliction = statusIcon.affliction; + Affliction affliction = statusIcon.Affliction; AfflictionPrefab afflictionPrefab = affliction.Prefab; Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); @@ -1059,10 +1081,10 @@ namespace Barotrauma { Rectangle glowRect = afflictionIconRect; glowRect.Inflate((int)(20 * GUI.Scale), (int)(20 * GUI.Scale)); - var glow = GUI.Style.GetComponentStyle("OuterGlowCircular"); + var glow = GUIStyle.GetComponentStyle("OuterGlowCircular"); glow.Sprites[GUIComponent.ComponentState.None][0].Draw( spriteBatch, glowRect, - GUI.Style.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); + GUIStyle.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); } float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; @@ -1082,8 +1104,8 @@ namespace Barotrauma if (highlightedAfflictionIcon != null) { - string nameTooltip = highlightedAfflictionIcon.Value.text; - Vector2 offset = GUI.Font.MeasureString(nameTooltip); + LocalizedString nameTooltip = highlightedAfflictionIcon.Value.NameToolTip; + Vector2 offset = GUIStyle.Font.MeasureString(nameTooltip); GUI.DrawString(spriteBatch, alignment == Alignment.Left ? highlightedIconPos + offset : highlightedIconPos - offset, @@ -1096,7 +1118,7 @@ namespace Barotrauma float currHealth = healthBar.BarSize; Color prevColor = healthBar.Color; healthBarShadow.BarSize = healthShadowSize; - healthBarShadow.Color = Color.Lerp(GUI.Style.Red, Color.Black, 0.5f); + healthBarShadow.Color = Color.Lerp(GUIStyle.Red, Color.Black, 0.5f); healthBarShadow.Visible = true; healthBar.BarSize = currHealth; healthBar.Color = prevColor; @@ -1113,7 +1135,7 @@ namespace Barotrauma float currHealth = healthWindowHealthBar.BarSize; Color prevColor = healthWindowHealthBar.Color; healthWindowHealthBarShadow.BarSize = healthShadowSize; - healthWindowHealthBarShadow.Color = GUI.Style.Red; + healthWindowHealthBarShadow.Color = GUIStyle.Red; healthWindowHealthBarShadow.Visible = true; healthWindowHealthBar.BarSize = currHealth; healthWindowHealthBar.Color = prevColor; @@ -1137,10 +1159,10 @@ namespace Barotrauma { if (prefab.IsBuff) { - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); } - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); } return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, prefab.IconColors); @@ -1213,7 +1235,7 @@ namespace Barotrauma CanBeFocused = false }; - var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.18f), content.RectTransform), 0.0f, GUI.Style.Green, style: "GUIAfflictionBar") + var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.18f), content.RectTransform), 0.0f, GUIStyle.Green, style: "GUIAfflictionBar") { UserData = "afflictionstrengthprediction", CanBeFocused = false @@ -1240,9 +1262,9 @@ namespace Barotrauma afflictionIcon.PressedColor = afflictionIcon.Color; afflictionIcon.HoverColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.6f); afflictionIcon.SelectedColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.5f); - + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.1f, 0.0f), content.RectTransform), - affliction.Prefab.Name, font: GUI.SmallFont, textAlignment: Alignment.BottomCenter) + affliction.Prefab.Name, font: GUIStyle.SmallFont, textAlignment: Alignment.BottomCenter) { CanBeFocused = false }; @@ -1274,13 +1296,13 @@ namespace Barotrauma //key = item identifier //float = suitability - Dictionary treatmentSuitability = new Dictionary(); + Dictionary treatmentSuitability = new Dictionary(); GetSuitableTreatments(treatmentSuitability, normalize: true, ignoreHiddenAfflictions: true, limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); - foreach (string treatment in treatmentSuitability.Keys.ToList()) + foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) { //prefer suggestions for items the player has if (Character.Controlled.Inventory.FindItemByIdentifier(treatment, recursive: true) != null) @@ -1304,10 +1326,10 @@ namespace Barotrauma recommendedTreatmentContainer.AutoHideScrollBar = true; } - List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); + List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); int count = 0; - foreach (KeyValuePair treatment in treatmentSuitabilities) + foreach (KeyValuePair treatment in treatmentSuitabilities) { count++; if (count > 5) { break; } @@ -1326,7 +1348,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (!(userdata is ItemPrefab itemPrefab)) { return false; } - var item = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); + var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); @@ -1337,15 +1359,15 @@ namespace Barotrauma new GUIImage(new RectTransform(Vector2.One, innerFrame.RectTransform, Anchor.Center), style: "TalentBackgroundGlow") { CanBeFocused = false, - Color = GUI.Style.Green, + Color = GUIStyle.Green, HoverColor = Color.White, PressedColor = Color.DarkGray, SelectedColor = Color.Transparent, DisabledColor = Color.Transparent }; - Sprite itemSprite = item.InventoryIcon ?? item.sprite; - Color itemColor = itemSprite == item.sprite ? item.SpriteColor : item.InventoryIconColor; + Sprite itemSprite = item.InventoryIcon ?? item.Sprite; + Color itemColor = itemSprite == item.Sprite ? item.SpriteColor : item.InventoryIconColor; var itemIcon = new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), innerFrame.RectTransform, Anchor.Center), itemSprite, scaleToFit: true) { @@ -1397,12 +1419,12 @@ namespace Barotrauma CanBeFocused = false }; - var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) + var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false, AutoScaleHorizontal = true }; - var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUI.SubHeadingFont) + var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { UserData = "strength", CanBeFocused = false @@ -1423,21 +1445,21 @@ namespace Barotrauma if (description.Font.MeasureString(description.WrappedText).Y > description.Rect.Height) { - description.Font = GUI.SmallFont; + description.Font = GUIStyle.SmallFont; } - Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUI.LargeFont.Size * 1.5f)); + Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f)); afflictionStrength.Text = strengthTexts[ MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; - Vector2 strengthDims = GUI.SubHeadingFont.MeasureString(afflictionStrength.Text); + Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text); labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, nameDims.Y)); afflictionName.RectTransform.Resize(new Point((int)(labelContainer.Rect.Width - strengthDims.X * 0.99f), nameDims.Y)); afflictionStrength.RectTransform.Resize(new Point(labelContainer.Rect.Width - afflictionName.Rect.Width, nameDims.Y)); - afflictionStrength.TextColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, + afflictionStrength.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); @@ -1451,8 +1473,8 @@ namespace Barotrauma { vitality.Visible = true; vitality.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; - vitality.TextColor = vitalityDecrease <= 0 ? GUI.Style.Green : - Color.Lerp(GUI.Style.Orange, GUI.Style.Red, affliction.Strength / affliction.Prefab.MaxStrength); + vitality.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green : + Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); } vitality.AutoDraw = true; @@ -1478,7 +1500,7 @@ namespace Barotrauma var potentialTreatment = Inventory.DraggingItems.FirstOrDefault(); if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab) { - potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); + potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); } potentialTreatment ??= Inventory.SelectedSlot?.Item; @@ -1488,11 +1510,11 @@ namespace Barotrauma Color afflictionEffectColor = Color.White; if (afflictionVitalityDecrease > 0.0f) { - afflictionEffectColor = GUI.Style.Red; + afflictionEffectColor = GUIStyle.Red; } else if (afflictionVitalityDecrease < 0.0f) { - afflictionEffectColor = GUI.Style.Green; + afflictionEffectColor = GUIStyle.Green; } var child = afflictionIconContainer.Content.FindChild(affliction); @@ -1510,7 +1532,7 @@ namespace Barotrauma if (afflictionStrengthPrediction < affliction.Strength) { afflictionStrengthBar.Color = afflictionEffectColor; - afflictionStrengthPredictionBar.Color = GUI.Style.Blue * t; + afflictionStrengthPredictionBar.Color = GUIStyle.Blue * t; afflictionStrengthPredictionBar.BarSize = afflictionStrengthBar.BarSize; afflictionStrengthBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; } @@ -1541,8 +1563,8 @@ namespace Barotrauma { foreach (var reduceAffliction in effect.ReduceAffliction) { - if (reduceAffliction.affliction != affliction.Identifier && reduceAffliction.affliction != affliction.Prefab.AfflictionType) { continue; } - strength -= reduceAffliction.amount * (effect.Duration > 0 ? effect.Duration : 1.0f); + if (reduceAffliction.AfflictionIdentifier != affliction.Identifier && reduceAffliction.AfflictionIdentifier != affliction.Prefab.AfflictionType) { continue; } + strength -= reduceAffliction.ReduceAmount * (effect.Duration > 0 ? effect.Duration : 1.0f); } foreach (var addAffliction in effect.Afflictions) { @@ -1563,7 +1585,7 @@ namespace Barotrauma strengthText.Text = strengthTexts[ MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; - strengthText.TextColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, + strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock; @@ -1576,8 +1598,8 @@ namespace Barotrauma { vitalityText.Visible = true; vitalityText.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; - vitalityText.TextColor = vitalityDecrease <= 0 ? GUI.Style.Green : - Color.Lerp(GUI.Style.Orange, GUI.Style.Red, affliction.Strength / affliction.Prefab.MaxStrength); + vitalityText.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green : + Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); } } @@ -1807,9 +1829,9 @@ namespace Barotrauma if (afflictionsDisplayedOnLimb.Count() > 1) { string additionalAfflictionCount = $"+{afflictionsDisplayedOnLimb.Count() - 1}"; - Vector2 displace = GUI.SubHeadingFont.MeasureString(additionalAfflictionCount); - GUI.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f); - GUI.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White); + Vector2 displace = GUIStyle.SubHeadingFont.MeasureString(additionalAfflictionCount); + GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f); + GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White); } i++; @@ -1885,7 +1907,7 @@ namespace Barotrauma for (int i = 0; i < afflictionCount; i++) { uint afflictionID = inc.ReadUInt32(); - AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UIntIdentifier == afflictionID); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionID); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); @@ -1913,7 +1935,7 @@ namespace Barotrauma { int limbIndex = inc.ReadRangedInteger(0, limbHealths.Count - 1); uint afflictionID = inc.ReadUInt32(); - AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UIntIdentifier == afflictionID); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionID); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs index 30833a339..f6030fad6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs @@ -2,14 +2,14 @@ { partial class DamageModifier { - [Serialize("", false), Editable] + [Serialize("", IsPropertySaveable.No), Editable] public string DamageSound { get; private set; } - [Serialize("", false), Editable] + [Serialize("", IsPropertySaveable.No), Editable] public string DamageParticle { get; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index 6e041eeac..19640dbc7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Barotrauma { - partial class JobPrefab : IPrefab, IDisposable + partial class JobPrefab : PrefabWithUintIdentifier { public GUIButton CreateInfoFrame(out GUIComponent buttonContainer) { @@ -18,20 +18,20 @@ namespace Barotrauma GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, height), frameHolder.RectTransform, Anchor.Center)); GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUIStyle.LargeFont); var descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.15f) }, - Description, font: GUI.SmallFont, wrap: true); + Description, font: GUIStyle.SmallFont, wrap: true); var skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.5f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform), - TextManager.Get("Skills"), font: GUI.LargeFont); + TextManager.Get("Skills"), font: GUIStyle.LargeFont); foreach (SkillPrefab skill in Skills) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), (int)skill.LevelRange.Start + " - " + (int)skill.LevelRange.End), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); } buttonContainer = paddedFrame; @@ -43,14 +43,14 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), - TextManager.Get("Items", fallBackTag: "mapentitycategory.equipment"), font: GUI.LargeFont); + TextManager.Get("Items", "mapentitycategory.equipment"), font: GUIStyle.LargeFont); foreach (string identifier in itemIdentifiers.Distinct()) { if (!(MapEntityPrefab.Find(name: null, identifier: identifier) is ItemPrefab itemPrefab)) { continue; } int count = itemIdentifiers.Count(i => i == identifier); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), " - " + (count == 1 ? itemPrefab.Name : itemPrefab.Name + " x" + count), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); }*/ return frameHolder; @@ -76,12 +76,12 @@ namespace Barotrauma } } - public List GetJobOutfitSprites(Gender gender, bool useInventoryIcon, out Vector2 maxDimensions) + public List GetJobOutfitSprites(CharacterInfoPrefab charInfoPrefab, bool useInventoryIcon, out Vector2 maxDimensions) { List outfitPreviews = new List(); maxDimensions = Vector2.One; - var equipIdentifiers = Element.GetChildElements("ItemSet").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeString("identifier", "")); + var equipIdentifiers = Element.GetChildElements("ItemSet").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeIdentifier("identifier", "")); List outfitPrefabs = new List(); foreach (var equipIdentifier in equipIdentifiers) @@ -114,8 +114,8 @@ namespace Barotrauma var children = previewElement.Elements().ToList(); for (int n = 0; n < children.Count; n++) { - XElement spriteElement = children[n]; - string spriteTexture = spriteElement.GetAttributeString("texture", "").Replace("[GENDER]", (gender == Gender.Female) ? "female" : "male"); + var spriteElement = children[n]; + string spriteTexture = charInfoPrefab.ReplaceVars(spriteElement.GetAttributeString("texture", ""), charInfoPrefab.Heads.First()); var sprite = new Sprite(spriteElement, file: spriteTexture); sprite.size = new Vector2(sprite.SourceRect.Width, sprite.SourceRect.Height); outfitPreview.AddSprite(sprite, children[n].GetAttributeVector2("offset", Vector2.Zero)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 15bd29bf4..4d924f605 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -78,7 +78,7 @@ namespace Barotrauma //{ // var pos = ConvertUnits.ToDisplayUnits(mouthPos.Value); // pos.Y = -pos.Y; - // ShapeExtensions.DrawPoint(spriteBatch, pos, GUI.Style.Red, size: 5); + // ShapeExtensions.DrawPoint(spriteBatch, pos, GUIStyle.Red, size: 5); //} // A debug visualisation on the bezier curve between limbs. @@ -95,7 +95,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, start, end, Color.White); GUI.DrawLine(spriteBatch, start, control, Color.Black); GUI.DrawLine(spriteBatch, control, end, Color.Black); - GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUI.Style.Red);*/ + GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUIStyle.Red);*/ } } @@ -274,7 +274,7 @@ namespace Barotrauma } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { for (int i = 0; i < Params.decorativeSpriteParams.Count; i++) { @@ -290,7 +290,7 @@ namespace Barotrauma spriteAnimState.Add(decorativeSprite, new SpriteState()); } TintMask = null; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -317,7 +317,7 @@ namespace Barotrauma NonConditionalDeformations.AddRange(deformations); break; case "randomcolor": - randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandom(); + randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandomUnsynced(); if (randomColor.HasValue) { Params.GetSprite().Color = randomColor.Value; @@ -337,8 +337,8 @@ namespace Barotrauma InitialLightSpriteAlpha = LightSource.OverrideLightSpriteAlpha; break; case "tintmask": - string tintMaskPath = subElement.GetAttributeString("texture", ""); - if (!string.IsNullOrWhiteSpace(tintMaskPath)) + ContentPath tintMaskPath = subElement.GetAttributeContentPath("texture"); + if (!tintMaskPath.IsNullOrWhiteSpace()) { TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath)); TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f); @@ -346,8 +346,8 @@ namespace Barotrauma } break; case "huskmask": - string huskMaskPath = subElement.GetAttributeString("texture", ""); - if (!string.IsNullOrWhiteSpace(huskMaskPath)) + ContentPath huskMaskPath = subElement.GetAttributeContentPath("texture"); + if (!huskMaskPath.IsNullOrWhiteSpace()) { HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath)); } @@ -387,7 +387,7 @@ namespace Barotrauma } if (deformation == null) { - deformation = SpriteDeformation.Load(animationElement, character.SpeciesName); + deformation = SpriteDeformation.Load(animationElement, character.SpeciesName.Value); if (deformation != null) { ragdoll.SpriteDeformations.Add(deformation); @@ -472,19 +472,23 @@ namespace Barotrauma } private string _texturePath; - private string GetSpritePath(XElement element, SpriteParams spriteParams) + private string GetSpritePath(ContentXElement element, SpriteParams spriteParams) { if (_texturePath == null) { if (spriteParams != null) { - string texturePath = character.Params.VariantFile?.Root?.GetAttributeString("texture", null) ?? spriteParams.GetTexturePath(); + ContentPath texturePath = + character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage) + ?? ContentPath.FromRaw(character.Prefab.ContentPackage, spriteParams.GetTexturePath()); _texturePath = GetSpritePath(texturePath); } else { - string texturePath = element.GetAttributeString("texture", null); - texturePath = string.IsNullOrWhiteSpace(texturePath) ? ragdoll.RagdollParams.Texture : texturePath; + ContentPath texturePath = element.GetAttributeContentPath("texture"); + texturePath = texturePath.IsNullOrWhiteSpace() + ? ContentPath.FromRaw(character.Prefab.ContentPackage, ragdoll.RagdollParams.Texture) + : texturePath; _texturePath = GetSpritePath(texturePath); } } @@ -494,20 +498,18 @@ namespace Barotrauma /// /// Get the full path of a limb sprite, taking into account tags, gender and head id /// - public static string GetSpritePath(string texturePath, CharacterInfo characterInfo) + public static string GetSpritePath(ContentPath texturePath, CharacterInfo characterInfo) { - string spritePath = texturePath; + string spritePath = texturePath.Value; string spritePathWithTags = spritePath; if (characterInfo != null) { - spritePath = spritePath.Replace("[GENDER]", (characterInfo.Gender == Gender.Female) ? "female" : "male"); - spritePath = spritePath.Replace("[RACE]", characterInfo.Race.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", characterInfo.HeadSpriteId.ToString()); + spritePath = characterInfo.ReplaceVars(spritePath); if (characterInfo.HeadSprite != null && characterInfo.SpriteTags.Any()) { string tags = ""; - characterInfo.SpriteTags.ForEach(tag => tags += "[" + tag + "]"); + characterInfo.SpriteTags.ForEach(tag => tags += $"[{tag}]"); spritePathWithTags = Path.Combine( Path.GetDirectoryName(spritePath), @@ -518,9 +520,9 @@ namespace Barotrauma } - private string GetSpritePath(string texturePath) + private string GetSpritePath(ContentPath texturePath) { - if (!character.IsHumanoid) { return texturePath; } + if (!character.IsHumanoid) { return texturePath.Value; } return GetSpritePath(texturePath, character?.Info); } @@ -696,7 +698,7 @@ namespace Barotrauma clr = clr.Multiply(ragdoll.RagdollParams.Color); if (character.Info != null) { - clr = clr.Multiply(character.Info.SkinColor); + clr = clr.Multiply(character.Info.Head.SkinColor); } if (character.CharacterHealth.FaceTint.A > 0 && type == LimbType.Head) { @@ -929,7 +931,7 @@ namespace Barotrauma if (pullJoint != null) { Vector2 pos = ConvertUnits.ToDisplayUnits(pullJoint.WorldAnchorB); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), GUIStyle.Red, true); } var bodyDrawPos = body.DrawPosition; bodyDrawPos.Y = -bodyDrawPos.Y; @@ -943,11 +945,11 @@ namespace Barotrauma var front = ConvertUnits.ToDisplayUnits(body.FarseerBody.GetWorldPoint(localFront)); front.Y = -front.Y; GUI.DrawLine(spriteBatch, bodyDrawPos, front, Color.Yellow, width: 2); - GUI.DrawLine(spriteBatch, from, to, GUI.Style.Red, width: 1); + GUI.DrawLine(spriteBatch, from, to, GUIStyle.Red, width: 1); GUI.DrawRectangle(spriteBatch, new Rectangle((int)from.X, (int)from.Y, 12, 12), Color.White, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 12, 12), Color.White, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)from.X, (int)from.Y, 10, 10), Color.Blue, true); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 10, 10), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 10, 10), GUIStyle.Red, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)front.X, (int)front.Y, 10, 10), Color.Yellow, true); //Vector2 mainLimbFront = ConvertUnits.ToDisplayUnits(ragdoll.MainLimb.body.FarseerBody.GetWorldPoint(ragdoll.MainLimb.body.GetFrontLocal(MathHelper.ToRadians(limbParams.Orientation)))); @@ -1046,8 +1048,8 @@ namespace Barotrauma //{ // width = (int)Math.Round(width / cam.Zoom); //} - //GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(up) * size, GUI.Style.Red, width: width); - Color color = modifier.DamageMultiplier > 1 ? GUI.Style.Red : GUI.Style.Green; + //GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(up) * size, GUIStyle.Red, width: width); + Color color = modifier.DamageMultiplier > 1 ? GUIStyle.Red : GUIStyle.Green; float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2); if (isScreenSpace) { @@ -1078,9 +1080,9 @@ namespace Barotrauma { wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, wearable.SheetIndex.Value), sprite.SourceRect.Size); } - else if (type == LimbType.Head && character.Info != null && character.Info.Head.SheetIndex.HasValue) + else if (type == LimbType.Head && character.Info != null) { - wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, character.Info.Head.SheetIndex.Value.ToPoint()), sprite.SourceRect.Size); + wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, character.Info.Head.SheetIndex.ToPoint()), sprite.SourceRect.Size); } else { @@ -1134,11 +1136,11 @@ namespace Barotrauma { if (wearable.Type == WearableType.Hair) { - wearableColor = character.Info.HairColor; + wearableColor = character.Info.Head.HairColor; } else if (wearable.Type == WearableType.Beard || wearable.Type == WearableType.Moustache) { - wearableColor = character.Info.FacialHairColor; + wearableColor = character.Info.Head.FacialHairColor; } } float scale = wearable.Scale; @@ -1164,22 +1166,22 @@ namespace Barotrauma wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); } - private WearableSprite GetWearableSprite(WearableType type, bool random = false) + private WearableSprite GetWearableSprite(WearableType type)//, bool random = false) { var info = character.Info; if (info == null) { return null; } - XElement element; - if (random) + ContentXElement element; + /*if (random) { - element = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Gender, info.Race), type, info.Head.HeadSpriteId)?.GetRandom(Rand.RandSync.ClientOnly); + element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet)?.GetRandom(Rand.RandSync.ClientOnly); } else - { - element = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Gender, info.Race), type, info.Head.HeadSpriteId)?.FirstOrDefault(); - } + {*/ + element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault(); + //} if (element != null) { - return new WearableSprite(element.Element("sprite"), type); + return new WearableSprite(element.GetChildElement("sprite"), type); } return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs new file mode 100644 index 000000000..166979300 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -0,0 +1,167 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; + +namespace Barotrauma +{ + public class ModProject + { + public class File + { + private File(string path, Type type) + { + Path = path.CleanUpPathCrossPlatform(correctFilenameCase: false); + Type = type switch + { + _ when !type.IsSubclassOf(typeof(ContentFile)) => throw new ArgumentException($"{type.Name} does not derive from {nameof(ContentFile)}"), + { IsAbstract: true } => throw new ArgumentException($"{type.Name} is abstract"), + _ => type + }; + } + + private File(ContentFile f) + { + Path = f.Path.RawValue ?? ""; + Type = f.GetType(); + } + + public static File FromContentFile(ContentFile file) + => new File(file); + + public static File FromPath(string path) where T : ContentFile + => new File(path, typeof(T)); + + /// + /// Prefer FromPath<T> when possible, this just exists + /// for cases where the type can only be decided at runtime + /// + public static File FromPath(string path, Type type) + => new File(path, type); + + public readonly string Path; + public readonly Type Type; + + public XElement ToXElement() + { + if (Type is null) { throw new InvalidOperationException("Type must be set before calling ToXElement"); } + if (Path.IsNullOrEmpty()) { throw new InvalidOperationException("Path must be set before calling ToXElement"); } + return new XElement(Type.Name.RemoveFromEnd("File"), new XAttribute("file", Path)); + } + } + + public ModProject() { } + + public ModProject(ContentPackage? contentPackage) + { + if (contentPackage is null) { return; } + Name = contentPackage.Name; + AltNames = contentPackage.AltNames.ToList(); + files = contentPackage.Files.Select(File.FromContentFile).ToList(); + ModVersion = IncrementModVersion(contentPackage.ModVersion); + IsCore = contentPackage is CorePackage; + SteamWorkshopId = contentPackage.SteamWorkshopId; + ExpectedHash = contentPackage.Hash; + InstallTime = contentPackage.InstallTime; + } + + private string name = ""; + public string Name + { + get => name; + set + { + var charsToRemove = Path.GetInvalidFileNameChars(); + name = string.Concat(value.Where(c => !charsToRemove.Contains(c))); + } + } + + public readonly List AltNames = new List(); + + private readonly List files = new List(); + public IReadOnlyList Files => files; + + public string ModVersion = ContentPackage.DefaultModVersion; + + public Md5Hash? ExpectedHash { get; private set; } + + public bool IsCore = false; + + public UInt64 SteamWorkshopId = 0; + + public DateTime? InstallTime = null; + + public bool HasFile(File file) + => Files.Any(f => + string.Equals(f.Path, file.Path, StringComparison.OrdinalIgnoreCase) + && f.Type == file.Type); + + public void AddFile(File file) + { + if (!HasFile(file)) + { + files.Add(file); + DiscardHashAndInstallTime(); + } + } + + public void DiscardHashAndInstallTime() + { + ExpectedHash = null; + InstallTime = null; + } + + public static string IncrementModVersion(string modVersion) + { + //look for an integer at the end of the string and increment it + int startIndex = modVersion.Length - 1; + while (char.IsDigit(modVersion[startIndex])) { startIndex--; } + startIndex++; + + if (startIndex >= modVersion.Length + || !char.IsDigit(modVersion[startIndex]) + || !int.TryParse( + modVersion[startIndex..], + NumberStyles.Any, + CultureInfo.InvariantCulture, + out int theFinalInteger)) + { + return modVersion; + } + + return $"{modVersion[..startIndex]}{(theFinalInteger + 1).ToString(CultureInfo.InvariantCulture)}"; + } + + public XDocument ToXDocument() + { + XDocument doc = new XDocument(); + XElement rootElement = new XElement("contentpackage"); + + void addRootAttribute(string name, T value) where T : notnull + => rootElement.Add(new XAttribute(name, value.ToString() ?? "")); + + addRootAttribute("name", Name); + addRootAttribute("modversion", ModVersion); + addRootAttribute("corepackage", IsCore); + if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); } + addRootAttribute("gameversion", GameMain.Version); + if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } + if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } + if (InstallTime != null) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value)); } + + files.ForEach(f => rootElement.Add(f.ToXElement())); + + doc.Add(rootElement); + return doc; + } + + public void Save(string path) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + ToXDocument().SaveSafe(path); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs new file mode 100644 index 000000000..3ae094982 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -0,0 +1,53 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.IO; +using Barotrauma.Steam; + +namespace Barotrauma +{ + public static partial class ContentPackageManager + { + public sealed partial class PackageSource : ICollection + { + public ContentPackage SaveAndEnableRegularMod(ModProject modProject) + { + if (modProject.IsCore) { throw new ArgumentException("ModProject must not be a core package"); } + + //save the content package + string fileListPath = Path.Combine(directory, ToolBox.RemoveInvalidFileNameChars(modProject.Name), ContentPackage.FileListFileName) + .CleanUpPathCrossPlatform(correctFilenameCase: false); + Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)!); + modProject.Save(fileListPath); + Refresh(); EnabledPackages.DisableRemovedMods(); + var newPackage = Regular.First(p => p.Path == fileListPath); + + //enable it + EnabledPackages.EnableRegular(newPackage); + + return newPackage; + } + } + + private static async Task> EnqueueWorkshopUpdates() + { + ISet subscribedItems = await SteamManager.Workshop.GetAllSubscribedItems(); + + var needInstalling = subscribedItems.Where(item + => !WorkshopPackages.Any(p + => item.Id == p.SteamWorkshopId + && p.InstallTime.HasValue + && item.LatestUpdateTime <= p.InstallTime)) + .ToArray(); + if (needInstalling.Any()) + { + await Task.WhenAll( + needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall)); + } + + return needInstalling; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index e4b05779f..22c5061a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -14,6 +14,7 @@ using FarseerPhysics; using Barotrauma.Extensions; using Barotrauma.Steam; using System.Threading.Tasks; +using Barotrauma.ClientSource.Settings; using Barotrauma.MapCreatures.Behavior; using static Barotrauma.FabricationRecipe; @@ -73,8 +74,6 @@ namespace Barotrauma private static readonly ChatManager chatManager = new ChatManager(true, 64); - public static Dictionary Keybinds = new Dictionary(); - public static void Init() { OpenAL.Alc.SetErrorReasonCallback((string msg) => NewMessage(msg, Color.Orange)); @@ -84,7 +83,7 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), frame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f }; - var toggleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform, Anchor.TopLeft), TextManager.Get("DebugConsoleHelpText"), Color.GreenYellow, GUI.SmallFont, Alignment.CenterLeft, style: null); + var toggleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform, Anchor.TopLeft), TextManager.Get("DebugConsoleHelpText"), Color.GreenYellow, GUIStyle.SmallFont, Alignment.CenterLeft, style: null); var closeButton = new GUIButton(new RectTransform(new Vector2(0.025f, 1.0f), toggleText.RectTransform, Anchor.TopRight), "X", style: null) { @@ -139,7 +138,7 @@ namespace Barotrauma var newMsg = queuedMessages.Dequeue(); AddMessage(newMsg); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(newMsg); if (unsavedMessages.Count >= messagesPerFile) @@ -153,9 +152,9 @@ namespace Barotrauma if (!IsOpen && GUI.KeyboardDispatcher.Subscriber == null) { - foreach (var (key, command) in Keybinds) + foreach (var (key, command) in DebugConsoleMapping.Instance.Bindings) { - if (PlayerInput.KeyHit(key)) + if (key.IsHit()) { ExecuteCommand(command); } @@ -275,7 +274,7 @@ namespace Barotrauma AddMessage(newMsg); } - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(newMsg); } @@ -313,7 +312,7 @@ namespace Barotrauma } }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, - msg.Text, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) + msg.Text, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -324,7 +323,7 @@ namespace Barotrauma else { var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - msg.Text, font: GUI.SmallFont, wrap: true) + msg.Text, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -355,7 +354,7 @@ namespace Barotrauma CanBeFocused = false }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 170, 0), textContainer.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(20, 0) }, - command.help, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) + command.help, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = Color.White @@ -398,17 +397,15 @@ namespace Barotrauma private static void InitProjectSpecific() { -#if WINDOWS commands.Add(new Command("copyitemnames", "", (string[] args) => { StringBuilder sb = new StringBuilder(); foreach (ItemPrefab mp in ItemPrefab.Prefabs) { - sb.AppendLine(mp.Name); + sb.AppendLine(mp.Name.Value); } Clipboard.SetText(sb.ToString()); })); -#endif commands.Add(new Command("autohull", "", (string[] args) => { @@ -534,8 +531,8 @@ namespace Barotrauma return; } - string subName = args.Length > 0 ? args[0] : ""; - if (string.IsNullOrWhiteSpace(subName)) + Identifier subName = (args.Length > 0 ? args[0] : "").ToIdentifier(); + if (subName.IsEmpty) { ThrowError("No submarine specified."); return; @@ -554,7 +551,7 @@ namespace Barotrauma levelGenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(p => p.Identifier == levelGenerationIdentifier); } - if (SubmarineInfo.SavedSubmarines.None(s => s.Name.ToLowerInvariant() == subName.ToLowerInvariant())) + if (SubmarineInfo.SavedSubmarines.None(s => s.Name == subName)) { ThrowError($"Cannot find a sub that matches the name \"{subName}\"."); return; @@ -566,8 +563,7 @@ namespace Barotrauma commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => { - SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; - SteamManager.SetSteamworksNetworkingDebugLog(SteamManager.NetworkingDebugLog); + SteamManager.SetSteamworksNetworkingDebugLog(!SteamManager.NetworkingDebugLog); })); commands.Add(new Command("readycheck", "Commence a ready check in multiplayer.", (string[] args) => @@ -586,27 +582,26 @@ namespace Barotrauma string keyString = args[0]; string command = args[1]; - if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object outKey) && outKey is Keys key) + KeyOrMouse key = Enum.TryParse(keyString, ignoreCase: true, out var outKey) + ? outKey + : Enum.TryParse(keyString, ignoreCase: true, out var outMouseButton) + ? outMouseButton + : (KeyOrMouse)MouseButton.None; + + if (key.Key == Keys.None && key.MouseButton == MouseButton.None) { - if (Keybinds.ContainsKey(key)) - { - Keybinds[key] = command; - } - else - { - Keybinds.Add(key, command); - } - NewMessage($"\"{command}\" bound to {key}.", GUI.Style.Green); - - if (GameMain.Config.keyMapping.FirstOrDefault(bind => bind.Key != Keys.None && bind.Key == key) is { } existingBind) - { - AddWarning($"\"{key}\" has already been bound to {(InputType)GameMain.Config.keyMapping.IndexOf(existingBind)}. The keybind will perform both actions when pressed."); - } - + ThrowError($"Invalid key {keyString}."); return; } + + DebugConsoleMapping.Instance.Set(key, command); + NewMessage($"\"{command}\" bound to {key}.", GUIStyle.Green); + + if (GameSettings.CurrentConfig.KeyMap.Bindings.FirstOrDefault(bind => bind.Value.Key != Keys.None && bind.Value.Key == key) is { } existingBind && existingBind.Value != null) + { + AddWarning($"\"{key}\" has already been bound to {existingBind.Key}. The keybind will perform both actions when pressed."); + } - ThrowError($"Invalid key {keyString}."); }, isCheat: false, getValidArgs: () => new[] { Enum.GetNames(typeof(Keys)), new[] { "\"\"" } })); commands.Add(new Command("unbindkey", "unbindkey [key]: Unbinds a command.", (string[] args) => @@ -618,40 +613,42 @@ namespace Barotrauma } string keyString = args[0]; - if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object outKey) && outKey is Keys key) + + KeyOrMouse key = Enum.TryParse(keyString, ignoreCase: true, out var outKey) + ? outKey + : Enum.TryParse(keyString, ignoreCase: true, out var outMouseButton) + ? outMouseButton + : (KeyOrMouse)MouseButton.None; + + if (key.Key == Keys.None && key.MouseButton == MouseButton.None) { - if (Keybinds.ContainsKey(key)) - { - Keybinds.Remove(key); - } - NewMessage("Keybind unbound.", GUI.Style.Green); + ThrowError($"Invalid key {keyString}."); return; } - ThrowError($"Invalid key {keyString}."); - }, isCheat: false, getValidArgs: () => new[] { Keybinds.Keys.Select(keys => keys.ToString()).Distinct().ToArray() })); + DebugConsoleMapping.Instance.Remove(key); + NewMessage("Keybind unbound.", GUIStyle.Green); + return; + }, isCheat: false, getValidArgs: () => new[] { DebugConsoleMapping.Instance.Bindings.Keys.Select(keys => keys.ToString()).Distinct().ToArray() })); commands.Add(new Command("savebinds", "savebinds: Writes current keybinds into the config file.", (string[] args) => { - ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({Keybinds.Count} keybind(s) assigned) Y/N", + ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({DebugConsoleMapping.Instance.Bindings.Count} keybind(s) assigned) Y/N", (option2) => { - if (option2.ToLowerInvariant() != "y") + if (option2.ToIdentifier() != "y") { - NewMessage("Aborted.", GUI.Style.Red); + NewMessage("Aborted.", GUIStyle.Red); return; } - GameSettings.ConsoleKeybinds = new Dictionary(Keybinds); - GameMain.Config.SaveNewPlayerConfig(); - - NewMessage($"{Keybinds.Count} keybind(s) written to the config file.", GUI.Style.Green); + GameSettings.SaveCurrentConfig(); }); }, isCheat: false)); commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) => { SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid; - NewMessage(SubEditorScreen.ShouldDrawGrid ? "Enabled submarine grid." : "Disabled submarine grid.", GUI.Style.Green); + NewMessage(SubEditorScreen.ShouldDrawGrid ? "Enabled submarine grid." : "Disabled submarine grid.", GUIStyle.Green); })); commands.Add(new Command("spreadsheetexport", "Export items in format recognized by the spreadsheet importer.", (string[] args) => @@ -801,7 +798,7 @@ namespace Barotrauma string colorString = string.Join(",", add ? args.SkipLast(1) : args); if (colorString.Equals("restore", StringComparison.OrdinalIgnoreCase)) { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.OriginalAmbientLight != null) { @@ -823,7 +820,7 @@ namespace Barotrauma GameMain.LightManager.AmbientLight = add ? GameMain.LightManager.AmbientLight.Add(color) : color; } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.OriginalAmbientLight ??= hull.AmbientLight; hull.AmbientLight = add ? hull.AmbientLight.Add(color) : color; @@ -987,14 +984,14 @@ namespace Barotrauma { if (entity is Item item) { - if (item.prefab.Identifier != args[0] && !item.Tags.Contains(args[0])) { continue; } + if (item.Prefab.Identifier != args[0] && !item.Tags.Contains(args[0])) { continue; } item.Reset(); if (MapEntity.SelectedList.Contains(item)) { item.CreateEditingHUD(); } entityFound = true; } else if (entity is Structure structure) { - if (structure.prefab.Identifier != args[0] && !structure.Tags.Contains(args[0])) { continue; } + if (structure.Prefab.Identifier != args[0] && !structure.Tags.Contains(args[0])) { continue; } structure.Reset(); if (MapEntity.SelectedList.Contains(structure)) { structure.CreateEditingHUD(); } entityFound = true; @@ -1018,7 +1015,7 @@ namespace Barotrauma { return new string[][] { - MapEntityPrefab.List.Select(me => me.Identifier).ToArray() + MapEntityPrefab.List.Select(me => me.Identifier.Value).ToArray() }; })); @@ -1103,11 +1100,6 @@ namespace Barotrauma } }, isCheat: true)); - commands.Add(new Command("tutorial", "", (string[] args) => - { - TutorialMode.StartTutorial(Tutorials.Tutorial.Tutorials[0]); - })); - commands.Add(new Command("save|savesub", "save [submarine name]: Save the currently loaded submarine using the specified name.", (string[] args) => { if (args.Length < 1) { return; } @@ -1196,7 +1188,7 @@ namespace Barotrauma var msgBox = new GUIMessageBox( args.Length > 0 ? args[0] : "", args.Length > 1 ? args[1] : "", - buttons: new string[] { "OK" }, + buttons: new LocalizedString[] { "OK" }, type: args.Length < 3 || args[2] == "default" ? GUIMessageBox.Type.Default : GUIMessageBox.Type.InGame); msgBox.Buttons[0].OnClicked = msgBox.Close; @@ -1217,10 +1209,12 @@ namespace Barotrauma { if (args.None() || !bool.TryParse(args[0], out bool state)) { - state = !GameMain.Config.DisableVoiceChatFilters; + state = !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; } - GameMain.Config.DisableVoiceChatFilters = state; - NewMessage("Voice chat filters " + (GameMain.Config.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); + var config = GameSettings.CurrentConfig; + config.Audio.DisableVoiceChatFilters = state; + GameSettings.SetCurrentConfig(config); + NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); }); AssignRelayToServer("togglevoicechatfilters", false); @@ -1345,7 +1339,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - fabricableItems.AddRange(itemPrefab.FabricationRecipes); + fabricableItems.AddRange(itemPrefab.FabricationRecipes.Values); } foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { @@ -1439,14 +1433,14 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab iPrefab in ItemPrefab.Prefabs) { - fabricableItems.AddRange(iPrefab.FabricationRecipes); + fabricableItems.AddRange(iPrefab.FabricationRecipes.Values); } string itemNameOrId = args[0].ToLowerInvariant(); ItemPrefab itemPrefab = (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + MapEntityPrefab.Find(null, identifier: itemNameOrId.ToIdentifier(), showErrorMessages: false)) as ItemPrefab; if (itemPrefab == null) { @@ -1459,7 +1453,7 @@ namespace Barotrauma // omega nesting incoming if (fabricationRecipe != null) { - foreach (KeyValuePair itemLocationPrice in itemPrefab.GetSellPricesOver(0)) + foreach (KeyValuePair itemLocationPrice in itemPrefab.GetSellPricesOver(0)) { NewMessage(" If bought at " + itemLocationPrice.Key + " it costs " + itemLocationPrice.Value.Price); int totalPrice = 0; @@ -1473,7 +1467,7 @@ namespace Barotrauma totalPrice += defaultPrice; totalBestPrice += ingredientItemPrefab.GetMinPrice(); int basePrice = defaultPrice; - foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) + foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) { if (basePrice > ingredientItemLocationPrice.Value.Price) { @@ -1499,7 +1493,7 @@ namespace Barotrauma }, () => { - return new string[][] { ItemPrefab.Prefabs.SelectMany(p => p.Aliases).Concat(ItemPrefab.Prefabs.Select(p => p.Identifier)).ToArray() }; + return new string[][] { ItemPrefab.Prefabs.SelectMany(p => p.Aliases).Concat(ItemPrefab.Prefabs.Select(p => p.Identifier.Value)).ToArray() }; }, isCheat: false)); commands.Add(new Command("checkcraftingexploits", "checkcraftingexploits: Finds outright item exploits created by buying store-bought ingredients and constructing them into sellable items.", (string[] args) => @@ -1507,7 +1501,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - fabricableItems.AddRange(itemPrefab.FabricationRecipes); + fabricableItems.AddRange(itemPrefab.FabricationRecipes.Values); } List> costDifferences = new List>(); @@ -1550,7 +1544,7 @@ namespace Barotrauma if (costDifference > maximumAllowedCost || costDifference < 0f) { float ratio = (float)fabricationCostStore.Value / defaultCost.Value; - string message = "Fabricating \"" + itemPrefab.Name + "\" costs " + (int)(ratio * 100) + "% of the price of the item, or " + costDifference + " more. Item price: " + defaultCost.Value + ", ingredient prices: " + fabricationCostStore.Value; + string message = $"Fabricating \"{itemPrefab.Name}\" costs {(int)(ratio * 100)}% of the price of the item, or {costDifference} more. Item price: {defaultCost.Value}, ingredient prices: {fabricationCostStore.Value}"; costDifferences.Add(new Tuple(message, costDifference)); } } @@ -1570,7 +1564,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab iP in ItemPrefab.Prefabs) { - fabricableItems.AddRange(iP.FabricationRecipes); + fabricableItems.AddRange(iP.FabricationRecipes.Values); } if (args.Length < 2) { @@ -1617,7 +1611,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab iP in ItemPrefab.Prefabs) { - fabricableItems.AddRange(iP.FabricationRecipes); + fabricableItems.AddRange(iP.FabricationRecipes.Values); } if (args.Length < 1) { @@ -1659,8 +1653,8 @@ namespace Barotrauma foreach (DeconstructItem deconstructItem in parentItem.DeconstructItems) { ItemPrefab itemPrefab = - (MapEntityPrefab.Find(deconstructItem.ItemIdentifier, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: deconstructItem.ItemIdentifier, showErrorMessages: false)) as ItemPrefab; + (MapEntityPrefab.Find(deconstructItem.ItemIdentifier.Value, identifier: null, showErrorMessages: false) ?? + MapEntityPrefab.Find(null, identifier: deconstructItem.ItemIdentifier, showErrorMessages: false)) as ItemPrefab; if (itemPrefab == null) { ThrowError($" Couldn't find deconstruct product \"{deconstructItem.ItemIdentifier}\"!"); @@ -1684,7 +1678,7 @@ namespace Barotrauma if (!(me is ISerializableEntity serializableEntity)) { continue; } if (serializableEntity.SerializableProperties == null) { continue; } - if (serializableEntity.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty property)) + if (serializableEntity.SerializableProperties.TryGetValue(args[0].ToIdentifier(), out SerializableProperty property)) { propertyFound = true; object prevValue = property.GetValue(me); @@ -1701,7 +1695,7 @@ namespace Barotrauma { foreach (ItemComponent ic in item.Components) { - ic.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty componentProperty); + ic.SerializableProperties.TryGetValue(args[0].ToIdentifier(), out SerializableProperty componentProperty); if (componentProperty == null) { continue; } propertyFound = true; object prevValue = componentProperty.GetValue(ic); @@ -1723,7 +1717,7 @@ namespace Barotrauma }, () => { - List propertyList = new List(); + List propertyList = new List(); foreach (MapEntity me in MapEntity.SelectedList) { if (!(me is ISerializableEntity serializableEntity)) { continue; } @@ -1740,55 +1734,63 @@ namespace Barotrauma return new string[][] { - propertyList.Distinct().ToArray(), - new string[0] + propertyList.Distinct().Select(i => i.Value).ToArray(), + Array.Empty() }; })); commands.Add(new Command("checkmissingloca", "", (string[] args) => { - //key = text tag, value = list of languages the tag is missing from - Dictionary> missingTags = new Dictionary>(); - Dictionary> tags = new Dictionary>(); - foreach (string language in TextManager.AvailableLanguages) + void SwapLanguage(LanguageIdentifier language) { - TextManager.Language = language; - tags.Add(language, new HashSet(TextManager.GetAllTagTextPairs().Select(t => t.Key))); + var config = GameSettings.CurrentConfig; + config.Language = language; + GameSettings.SetCurrentConfig(config); + } + + //key = text tag, value = list of languages the tag is missing from + Dictionary> missingTags = new Dictionary>(); + Dictionary> tags = new Dictionary>(); + foreach (LanguageIdentifier language in TextManager.AvailableLanguages) + { + SwapLanguage(language); + tags.Add(language, new HashSet(TextManager.GetAllTagTextPairs().Select(t => t.Key))); } - foreach (string language in TextManager.AvailableLanguages) + foreach (LanguageIdentifier language in TextManager.AvailableLanguages) { //check missing mission texts - foreach (var missionPrefab in MissionPrefab.List) + foreach (var missionPrefab in MissionPrefab.Prefabs) { - string missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeString("textidentifier", string.Empty)); - string nameIdentifier = "missionname." + missionId; + Identifier missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); + Identifier nameIdentifier = $"missionname.{missionId}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - string descriptionIdentifier = "missiondescription." + missionId; + Identifier descriptionIdentifier = $"missiondescription.{missionId}".ToIdentifier(); if (!tags[language].Contains(descriptionIdentifier)) { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } missingTags[descriptionIdentifier].Add(language); } } foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { - if (sub.Type != SubmarineType.Player) { continue; } - string nameIdentifier = "submarine.name." + sub.Name.ToLowerInvariant(); + if (sub.Type != SubmarineType.Player || !sub.IsVanillaSubmarine()) { continue; } + + Identifier nameIdentifier = $"submarine.name.{sub.Name}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - string descriptionIdentifier = "submarine.description." + sub.Name.ToLowerInvariant(); + Identifier descriptionIdentifier = ("submarine.description." + sub.Name).ToIdentifier(); if (!tags[language].Contains(descriptionIdentifier)) { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } missingTags[descriptionIdentifier].Add(language); } } @@ -1803,18 +1805,18 @@ namespace Barotrauma continue; } - string afflictionId = affliction.TranslationOverride ?? affliction.Identifier; - string nameIdentifier = "afflictionname." + afflictionId; + Identifier afflictionId = affliction.TranslationIdentifier; + Identifier nameIdentifier = $"afflictionname.{afflictionId}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - - string descriptionIdentifier = "afflictiondescription." + afflictionId; + + Identifier descriptionIdentifier = $"afflictiondescription.{afflictionId}".ToIdentifier(); if (!tags[language].Contains(descriptionIdentifier)) { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } missingTags[descriptionIdentifier].Add(language); } } @@ -1823,10 +1825,10 @@ namespace Barotrauma { foreach (var talentSubTree in talentTree.TalentSubTrees) { - string nameIdentifier = "talenttree." + talentSubTree.Identifier; + Identifier nameIdentifier = $"talenttree.{talentSubTree.Identifier}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } } @@ -1834,10 +1836,10 @@ namespace Barotrauma foreach (var talent in TalentPrefab.TalentPrefabs) { - string nameIdentifier = "talentname." + talent.Identifier; + Identifier nameIdentifier = $"talentname.{talent.Identifier}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } } @@ -1845,43 +1847,138 @@ namespace Barotrauma //check missing entity names foreach (MapEntityPrefab me in MapEntityPrefab.List) { - string nameIdentifier = "entityname." + me.Identifier; + Identifier nameIdentifier = ("entityname." + me.Identifier).ToIdentifier(); if (tags[language].Contains(nameIdentifier)) { continue; } if (me is ItemPrefab itemPrefab) { - nameIdentifier = itemPrefab.ConfigElement?.GetAttributeString("nameidentifier", null) ?? nameIdentifier; + nameIdentifier = itemPrefab.ConfigElement?.GetAttributeIdentifier("nameidentifier", nameIdentifier) ?? nameIdentifier; if (nameIdentifier != null) { if (tags[language].Contains("entityname." + nameIdentifier)) { continue; } } } - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } } - foreach (string englishTag in tags["English"]) + foreach (Identifier englishTag in tags[TextManager.DefaultLanguage]) { - foreach (string language in TextManager.AvailableLanguages) + foreach (LanguageIdentifier language in TextManager.AvailableLanguages) { - if (language == "English") { continue; } + if (language == TextManager.DefaultLanguage) { continue; } if (!tags[language].Contains(englishTag)) { - if (!missingTags.ContainsKey(englishTag)) { missingTags[englishTag] = new HashSet(); } + if (!missingTags.ContainsKey(englishTag)) { missingTags[englishTag] = new HashSet(); } missingTags[englishTag].Add(language); } } } - List lines = missingTags.Select(t => "\"" + t.Key + "\"\n missing from " + string.Join(", ", t.Value)).ToList(); + List lines = new List + { + "Missing from English:" + }; + Dictionary> missingByLanguages = new Dictionary>(); + List missingFromEnglish = new List(); + foreach (KeyValuePair> kvp in missingTags) + { + if (kvp.Value.Contains(TextManager.DefaultLanguage)) + { + missingFromEnglish.Add(kvp.Key.Value); + } + else + { + string languagesStr = string.Join(", ", kvp.Value.OrderBy(v => v.Value.Value)); + if (!missingByLanguages.ContainsKey(languagesStr)) + { + missingByLanguages.Add(languagesStr, new List()); + } + missingByLanguages[languagesStr].Add(kvp.Key.Value); + } + } + foreach (string text in missingFromEnglish.OrderBy(v => v)) + { + lines.Add(text); + } + + foreach (KeyValuePair> missingByLanguage in missingByLanguages) + { + lines.Add(string.Empty); + lines.Add($"Missing from {missingByLanguage.Key}"); + foreach (string text in missingByLanguage.Value.OrderBy(v => v)) + { + lines.Add(text); + } + } + string filePath = "missingloca.txt"; Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); - TextManager.Language = "English"; + SwapLanguage(TextManager.DefaultLanguage); + })); + + commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) => + { + if (args.Length < 2) + { + ThrowError("Please specify two files two compare."); + return; + } + + XDocument doc1 = XMLExtensions.TryLoadXml(args[0]); + if (doc1?.Root == null) + { + ThrowError($"Could not load the file \"{args[0]}\""); + return; + } + XDocument doc2 = XMLExtensions.TryLoadXml(args[1]); + if (doc2?.Root == null) + { + ThrowError($"Could not load the file \"{args[1]}\""); + return; + } + + var content1 = getContent(doc1.Root); + var content2 = getContent(doc2.Root); + + foreach (KeyValuePair kvp in content1) + { + if (!content2.ContainsKey(kvp.Key)) + { + ThrowError($"File 2 doesn't contain the text tag \"{kvp.Key}\""); + } + else + { + if (content2[kvp.Key] != kvp.Value) + { + ThrowError($"Texts for the tag \"{kvp.Key}\" don't match:\n1. {kvp.Value}\n2. {content2[kvp.Key]}"); + } + } + } + foreach (KeyValuePair kvp in content2) + { + if (!content1.ContainsKey(kvp.Key)) + { + ThrowError($"File 1 doesn't contain the text tag \"{kvp.Key}\""); + } + } + + static Dictionary getContent(XElement element) + { + Dictionary content = new Dictionary(); + foreach (XElement subElement in element.Elements()) + { + string key = subElement.Name.ToString().ToLowerInvariant(); + if (content.ContainsKey(key)) { continue; } + content.Add(key, subElement.ElementInnerText()); + } + return content; + } })); commands.Add(new Command("eventstats", "", (string[] args) => @@ -1959,7 +2056,7 @@ namespace Barotrauma commands.Add(new Command("showballastflorasprite", "", (string[] args) => { BallastFloraBehavior.AlwaysShowBallastFloraSprite = !BallastFloraBehavior.AlwaysShowBallastFloraSprite; - NewMessage("ok", GUI.Style.Green); + NewMessage("ok", GUIStyle.Green); })); commands.Add(new Command("printreceivertransfers", "", (string[] args) => @@ -2046,8 +2143,8 @@ namespace Barotrauma if (mapEntity is Item item) { item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, - (int)(item.Prefab.sprite.size.X * item.Prefab.Scale), - (int)(item.Prefab.sprite.size.Y * item.Prefab.Scale)); + (int)(item.Prefab.Sprite.size.X * item.Prefab.Scale), + (int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale)); } else if (mapEntity is Structure structure) { @@ -2196,8 +2293,8 @@ namespace Barotrauma List lines = new List(); foreach (MapEntityPrefab me in MapEntityPrefab.List) { - lines.Add("" + me.Name + ""); - lines.Add("" + me.Description + ""); + lines.Add($"{me.Name}"); + lines.Add($"{me.Description}"); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); @@ -2213,12 +2310,12 @@ namespace Barotrauma foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs()) { - if (string.IsNullOrEmpty(eventPrefab.Identifier)) + if (eventPrefab.Identifier.IsEmpty) { continue; } docs.Add(eventPrefab.ConfigElement.Document); - getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier); + getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier.Value); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); @@ -2250,7 +2347,7 @@ namespace Barotrauma } int i = 1; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -2326,7 +2423,7 @@ namespace Barotrauma - Dictionary dictionary = new Dictionary(); + Dictionary dictionary = new Dictionary(); foreach (var property in properties) { object[] attributes = property.GetCustomAttributes(true); @@ -2347,10 +2444,10 @@ namespace Barotrauma } propertyTypeName = string.Join("/", valueNames); } - string defaultValueString = serialize.defaultValue?.ToString() ?? ""; + string defaultValueString = serialize.DefaultValue?.ToString() ?? ""; if (property.PropertyType == typeof(float)) { - defaultValueString = ((float)serialize.defaultValue).ToString(CultureInfo.InvariantCulture); + defaultValueString = ((float)serialize.DefaultValue).ToString(CultureInfo.InvariantCulture); } lines.Add(" [tr]"); @@ -2412,7 +2509,7 @@ namespace Barotrauma commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => { if (args.Length != 1) return; - TextManager.CheckForDuplicates(args[0]); + TextManager.CheckForDuplicates(args[0].ToIdentifier().ToLanguageIdentifier()); })); commands.Add(new Command("writetocsv|xmltocsv", "Writes the default language (English) to a .csv file.", (string[] args) => @@ -2470,8 +2567,8 @@ namespace Barotrauma { var property = allProperties[j].Second; string propertyName = (allProperties[j].First.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); - string displayName = TextManager.Get($"sp.{propertyName}.name", returnNull: true); - if (displayName == null) + LocalizedString displayName = TextManager.Get($"sp.{propertyName}.name"); + if (displayName.IsNullOrEmpty()) { displayName = property.Name.FormatCamelCaseWithSpaces(); @@ -2494,26 +2591,28 @@ namespace Barotrauma commands.Add(new Command("cleanbuild", "", (string[] args) => { - GameMain.Config.MusicVolume = 0.5f; - GameMain.Config.SoundVolume = 0.5f; - GameMain.Config.DynamicRangeCompressionEnabled = true; - GameMain.Config.VoipAttenuationEnabled = true; + /*GameSettings.CurrentConfig.MusicVolume = 0.5f; + GameSettings.CurrentConfig.SoundVolume = 0.5f; + GameSettings.CurrentConfig.DynamicRangeCompressionEnabled = true; + GameSettings.CurrentConfig.VoipAttenuationEnabled = true; NewMessage("Music and sound volume set to 0.5", Color.Green); - GameMain.Config.GraphicsWidth = 0; - GameMain.Config.GraphicsHeight = 0; - GameMain.Config.WindowMode = WindowMode.BorderlessWindowed; + GameSettings.CurrentConfig.GraphicsWidth = 0; + GameSettings.CurrentConfig.GraphicsHeight = 0; + GameSettings.CurrentConfig.WindowMode = WindowMode.BorderlessWindowed; NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green); NewMessage("Fullscreen enabled", Color.Green); - GameSettings.VerboseLogging = false; + GameSettings.CurrentConfig.VerboseLogging = false; - if (GameMain.Config.MasterServerUrl != "http://www.undertowgames.com/baromaster") + if (GameSettings.CurrentConfig.MasterServerUrl != "http://www.undertowgames.com/baromaster") { - ThrowError("MasterServerUrl \"" + GameMain.Config.MasterServerUrl + "\"!"); + ThrowError("MasterServerUrl \"" + GameSettings.CurrentConfig.MasterServerUrl + "\"!"); } - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig();*/ + throw new NotImplementedException(); + #warning TODO: reimplement var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder); @@ -2606,10 +2705,11 @@ namespace Barotrauma return; } - GameMain.Config.SelectCorePackage(GameMain.Config.CurrentCorePackage, true); + ContentPackageManager.EnabledPackages.ReloadCore(); })); - commands.Add(new Command("ingamemodswap", "", (string[] args) => + #warning TODO: reimplement? + /*commands.Add(new Command("ingamemodswap", "", (string[] args) => { ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap; if (ContentPackage.IngameModSwap) @@ -2620,7 +2720,7 @@ namespace Barotrauma { NewMessage("Disabled ingame mod swapping"); } - })); + }));*/ AssignOnClientExecute( "giveperm", @@ -2820,7 +2920,7 @@ namespace Barotrauma ThrowError("Please give the location type after the command."); return; } - var locationType = LocationType.List.Find(lt => lt.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == args[0]); if (locationType == null) { ThrowError($"Could not find the location type \"{args[0]}\"."); @@ -2832,7 +2932,7 @@ namespace Barotrauma { return new string[][] { - LocationType.List.Select(lt => lt.Identifier).ToArray() + LocationType.Prefabs.Select(lt => lt.Identifier.Value).ToArray() }; })); #endif @@ -3028,67 +3128,6 @@ namespace Barotrauma if (Submarine.MainSub.SubBody != null) { Submarine.MainSub?.FlipX(); } }, isCheat: true)); - commands.Add(new Command("gender", "Set the gender of the controlled character. Allowed parameters: Male, Female, None.", args => - { - var character = Character.Controlled; - if (character == null) - { - ThrowError("Not controlling any character!"); - return; - } - if (args.Length == 0) - { - ThrowError("No parameters provided!"); - return; - } - if (Enum.TryParse(args[0], true, out Gender gender)) - { - character.Info.Gender = gender; - character.ReloadHead(); - foreach (var limb in character.AnimController.Limbs) - { - if (limb.type != LimbType.Head) - { - limb.RecreateSprites(); - } - foreach (var wearable in limb.WearingItems) - { - if (wearable.Gender != Gender.None && wearable.Gender != gender) - { - wearable.Gender = gender; - } - } - } - } - }, isCheat: true)); - - commands.Add(new Command("race", "Set race of the controlled character. Allowed parameters: White, Black, Asian, None.", args => - { - var character = Character.Controlled; - if (character == null) - { - ThrowError("Not controlling any character!"); - return; - } - if (args.Length == 0) - { - ThrowError("No parameters provided!"); - return; - } - if (Enum.TryParse(args[0], true, out Race race)) - { - character.Info.Race = race; - character.ReloadHead(); - foreach (var limb in character.AnimController.Limbs) - { - if (limb.type != LimbType.Head) - { - limb.RecreateSprites(); - } - } - } - }, isCheat: true)); - commands.Add(new Command("head", "Load the head sprite and the wearables (hair etc). Required argument: head id. Optional arguments: hair index, beard index, moustache index, face attachment index.", args => { var character = Character.Controlled; @@ -3188,7 +3227,7 @@ namespace Barotrauma { return new string[][] { - SubmarineInfo.SavedSubmarines.Select(s => s.DisplayName).ToArray() + SubmarineInfo.SavedSubmarines.Select(s => s.DisplayName.Value).ToArray() }; }, isCheat: true)); @@ -3276,7 +3315,7 @@ namespace Barotrauma } case "identifier": case "id": - sprites = Sprite.LoadedSprites.Where(s => s.EntityID != null && s.EntityID.Equals(secondArg, StringComparison.OrdinalIgnoreCase)); + sprites = Sprite.LoadedSprites.Where(s => s.EntityIdentifier != null && s.EntityIdentifier == secondArg); if (sprites.Any()) { foreach (var s in sprites) @@ -3377,7 +3416,7 @@ namespace Barotrauma PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price, adjustDown, depth, adjustItemType); break; case AdjustItemTypes.Additive: - PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price + (int)((newPrice - materialPrefab.DefaultPrice.Price) / (double)fabricationRecipe.RequiredItems.Count), adjustDown, depth, adjustItemType); + PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price + (int)((newPrice - materialPrefab.DefaultPrice.Price) / (double)fabricationRecipe.RequiredItems.Length), adjustDown, depth, adjustItemType); break; case AdjustItemTypes.Multiplicative: PrintItemCosts(newPrices, itemPrefab, fabricableItems, (int)(itemPrefab.DefaultPrice.Price * newPriceMult), adjustDown, depth, adjustItemType); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 781d2c3ba..7f8c08485 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -94,7 +94,7 @@ namespace Barotrauma var (relative, min) = GetSizes(dialogType); - GUIMessageBox messageBox = new GUIMessageBox(string.Empty, string.Empty, new string[0], + GUIMessageBox messageBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize: relative, minSize: min, type: GUIMessageBox.Type.InGame, backgroundIcon: EventSet.GetEventSprite(spriteIdentifier)) { @@ -105,7 +105,7 @@ namespace Barotrauma messageBox.InnerFrame.ClearChildren(); messageBox.AutoClose = false; - GUI.Style.Apply(messageBox.InnerFrame, "DialogBox"); + GUIStyle.Apply(messageBox.InnerFrame, "DialogBox"); if (actionInstance != null) { @@ -222,11 +222,11 @@ namespace Barotrauma closeButton.SlideIn(0.5f, 0.33f, 16, SlideDirection.Down); InputType? closeInput = null; - if (GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None) { closeInput = InputType.Use; } - else if (GameMain.Config.KeyBind(InputType.Select).MouseButton == MouseButton.None) + else if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select].MouseButton == MouseButton.None) { closeInput = InputType.Select; } @@ -239,7 +239,7 @@ namespace Barotrauma { GUIButton btn = component as GUIButton; btn?.OnClicked(btn, btn.UserData); - btn?.Flash(GUI.Style.Green); + btn?.Flash(GUIStyle.Green); } }; } @@ -308,7 +308,7 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - string translatedText = TextManager.Get(text, returnNull: true) ?? text; + LocalizedString translatedText = TextManager.Get(text); if (speaker?.Info != null && drawChathead) { @@ -335,9 +335,9 @@ namespace Barotrauma { foreach (string option in options) { - var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option, returnNull: true) ?? option, style: "ListBoxElement"); + var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option), style: "ListBoxElement"); btn.TextBlock.TextAlignment = Alignment.CenterLeft; - btn.TextColor = btn.HoverTextColor = GUI.Style.Green; + btn.TextColor = btn.HoverTextColor = GUIStyle.Green; btn.TextBlock.Wrap = true; buttons.Add(btn); } @@ -384,7 +384,7 @@ namespace Barotrauma } // Too broken, left it here if I ever want to come back to it - private static List GetQuoteHighlights(string text, Color color) + /*private static List GetQuoteHighlights(string text, Color color) { char[] quotes = { '“', '”', '\"', '\'', '「', '」'}; @@ -406,6 +406,6 @@ namespace Barotrauma last.EndIndex = text.Length; } return textColors; - } + }*/ } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 238445418..9e678f095 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -34,7 +35,7 @@ namespace Barotrauma var textOffset = new Vector2(-150, 0); spriteBatch.DrawCircle(drawPos, 600, 6, Color.White, thickness: 20); - GUI.DrawString(spriteBatch, drawPos + textOffset, ev.ToString(), Color.White, Color.Black, 0, GUI.LargeFont); + GUI.DrawString(spriteBatch, drawPos + textOffset, ev.ToString(), Color.White, Color.Black, 0, GUIStyle.LargeFont); } } @@ -46,24 +47,23 @@ namespace Barotrauma } float theoreticalMaxMonsterStrength = 10000; - float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * GameMain.GameSession.LevelData.Difficulty / 100; + float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * (GameMain.GameSession?.LevelData?.Difficulty ?? 0f) / 100; float absoluteMonsterStrength = monsterStrength / theoreticalMaxMonsterStrength; float relativeMonsterStrength = monsterStrength / relativeMaxMonsterStrength; - GUI.DrawString(spriteBatch, new Vector2(10, y), "EventManager", Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 20), "Event cooldown: " + (int)Math.Max(eventCoolDown, 0), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 35), "Current intensity: " + (int)Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, currentIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 50), "Target intensity: " + (int)Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, targetIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(10, y), "EventManager", Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 20), "Event cooldown: " + (int)Math.Max(eventCoolDown, 0), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 35), "Current intensity: " + (int)Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, currentIntensity), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 50), "Target intensity: " + (int)Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, targetIntensity), Color.Black * 0.6f, 0, GUIStyle.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "Crew health: " + (int)Math.Round(avgCrewHealth * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "Hull integrity: " + (int)Math.Round(avgHullIntegrity * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "Flooding amount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, floodingAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "Fire amount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, fireAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "Enemy danger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 140), "Current monster strength (total): " + (int)Math.Round(monsterStrength), Color.Lerp(GUI.Style.Green, GUI.Style.Red, relativeMonsterStrength), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 155), "Main events: " + (int)Math.Round(CumulativeMonsterStrengthMain), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 170), "Ruin events: " + (int)Math.Round(CumulativeMonsterStrengthRuins), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 185), "Wreck events: " + (int)Math.Round(CumulativeMonsterStrengthWrecks), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 200), "Cave events: " + (int)Math.Round(CumulativeMonsterStrengthCaves), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "Crew health: " + (int)Math.Round(avgCrewHealth * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "Hull integrity: " + (int)Math.Round(avgHullIntegrity * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "Flooding amount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, floodingAmount), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "Fire amount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, fireAmount), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "Enemy danger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, enemyDanger), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 140), "Current monster strength (total): " + (int)Math.Round(monsterStrength), Color.Lerp(GUIStyle.Green, GUIStyle.Red, relativeMonsterStrength), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 155), "Main events: " + (int)Math.Round(CumulativeMonsterStrengthMain), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 170), "Ruin events: " + (int)Math.Round(CumulativeMonsterStrengthRuins), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 185), "Wreck events: " + (int)Math.Round(CumulativeMonsterStrengthWrecks), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); #if DEBUG if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) && @@ -101,7 +101,7 @@ namespace Barotrauma { isGraphSelected = false; } - Color intensityColor = Color.Lerp(Color.White, GUI.Style.Red, currentIntensity); + Color intensityColor = Color.Lerp(Color.White, GUIStyle.Red, currentIntensity); if (isGraphHovered || isGraphSelected) { graphRect.Size = new Point(GameMain.GraphicsWidth - 30, (int)(GameMain.GraphicsHeight * 0.35f)); @@ -122,7 +122,7 @@ namespace Barotrauma { height *= 3; string text = (order / 6).ToString(); - var font = GUI.SmallFont; + var font = GUIStyle.SmallFont; Vector2 textSize = font.MeasureString(text); Vector2 textPos = new Vector2(bottomPoint.X - textSize.X / 2, bottomPoint.Y + height * 1.5f); GUI.DrawString(sBatch, textPos, text, Color.White, font: font); @@ -175,23 +175,23 @@ namespace Barotrauma int x = graphRect.X; if (isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) { - GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew away from sub): " + ToolBox.SecondsToReadableTime(settings.FreezeDurationWhenCrewAway - crewAwayDuration), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew away from sub): " + ToolBox.SecondsToReadableTime(settings.FreezeDurationWhenCrewAway - crewAwayDuration), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } else if (crewAwayResetTimer > 0.0f) { - GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew just returned to the sub): " + ToolBox.SecondsToReadableTime(crewAwayResetTimer), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew just returned to the sub): " + ToolBox.SecondsToReadableTime(crewAwayResetTimer), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } else if (eventCoolDown > 0.0f) { - GUI.DrawString(spriteBatch, new Vector2(x, y), "Event cooldown active: " + ToolBox.SecondsToReadableTime(eventCoolDown), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Event cooldown active: " + ToolBox.SecondsToReadableTime(eventCoolDown), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } else if (currentIntensity > eventThreshold) { GUI.DrawString(spriteBatch, new Vector2(x, y), - "Intensity too high for new events: " + (int)(currentIntensity * 100) + "%/" + (int)(eventThreshold * 100) + "%", Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + "Intensity too high for new events: " + (int)(currentIntensity * 100) + "%/" + (int)(eventThreshold * 100) + "%", Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } @@ -199,29 +199,29 @@ namespace Barotrauma { if (Submarine.MainSub == null) { break; } - GUI.DrawString(spriteBatch, new Vector2(x, y), "New event (ID " + eventSet.DebugIdentifier + ") after: ", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "New event (ID " + eventSet.Identifier + ") after: ", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; if (eventSet.PerCave) { - GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near cave", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near cave", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } if (eventSet.PerWreck) { - GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } if (eventSet.PerRuin) { - GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } if (roundDuration < eventSet.MinMissionTime) { GUI.DrawString(spriteBatch, new Vector2(x, y), " " + (int) (eventSet.MinDistanceTraveled * 100.0f) + "% travelled (current: " + (int) (distanceTraveled * 100.0f) + " %)", - ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) ? Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, eventSet.MinDistanceTraveled - distanceTraveled) : GUI.Style.Green) * 0.8f, null, 0, GUI.SmallFont); + ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) ? Color.Lerp(GUIStyle.Yellow, GUIStyle.Red, eventSet.MinDistanceTraveled - distanceTraveled) : GUIStyle.Green) * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } @@ -229,15 +229,15 @@ namespace Barotrauma { GUI.DrawString(spriteBatch, new Vector2(x, y), " intensity between " + eventSet.MinIntensity.FormatDoubleDecimal() + " and " + eventSet.MaxIntensity.FormatDoubleDecimal(), - Color.Orange * 0.8f, null, 0, GUI.SmallFont); - y += 12; + Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); + y += 12; } if (roundDuration < eventSet.MinMissionTime) { GUI.DrawString(spriteBatch, new Vector2(x, y), " " + (int) (eventSet.MinMissionTime - roundDuration) + " s", - Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, (eventSet.MinMissionTime - roundDuration)), null, 0, GUI.SmallFont); + Color.Lerp(GUIStyle.Yellow, GUIStyle.Red, (eventSet.MinMissionTime - roundDuration)), null, 0, GUIStyle.SmallFont); } y += 15; @@ -249,14 +249,14 @@ namespace Barotrauma } } - GUI.DrawString(spriteBatch, new Vector2(x, y), "Current events: ", Color.White * 0.9f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Current events: ", Color.White * 0.9f, null, 0, GUIStyle.SmallFont); y += yStep; foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) { - GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont); - Rectangle rect = new Rectangle(new Point(x + 5, y), GUI.SmallFont.MeasureString(ev.ToString()).ToPoint()); + Rectangle rect = new Rectangle(new Point(x + 5, y), GUIStyle.SmallFont.MeasureString(ev.ToString()).ToPoint()); Rectangle outlineRect = new Rectangle(rect.Location, rect.Size); outlineRect.Inflate(4, 4); @@ -331,8 +331,8 @@ namespace Barotrauma if (Screen.Selected is GameScreen screen) { Camera cam = screen.Cam; - Dictionary> tagsDictionary = new Dictionary>(); - foreach ((string key, List value) in scriptedEvent.Targets) + Dictionary> tagsDictionary = new Dictionary>(); + foreach ((Identifier key, List value) in scriptedEvent.Targets) { foreach (Entity entity in value) { @@ -342,24 +342,24 @@ namespace Barotrauma } else { - tagsDictionary.Add(entity, new List { key }); + tagsDictionary.Add(entity, new List { key }); } } } - string identifier = scriptedEvent.Prefab.Identifier; + Identifier identifier = scriptedEvent.Prefab.Identifier; - foreach ((Entity entity, List tags) in tagsDictionary) + foreach ((Entity entity, List tags) in tagsDictionary) { if (entity.Removed) { continue; } string text = tags.Aggregate("Tags:\n", (current, tag) => current + $" {tag.ColorizeObject()}\n").TrimEnd('\r', '\n'); - if (!string.IsNullOrWhiteSpace(identifier)) { text = $"Event: {identifier.ColorizeObject()}\n{text}"; } + if (!identifier.IsEmpty) { text = $"Event: {identifier.ColorizeObject()}\n{text}"; } - List richTextData = RichTextData.GetRichTextData(text, out text); + ImmutableArray? richTextData = RichTextData.GetRichTextData(text, out text); Vector2 entityPos = cam.WorldToScreen(entity.WorldPosition); - Vector2 infoSize = GUI.SmallFont.MeasureString(text); + Vector2 infoSize = GUIStyle.SmallFont.MeasureString(text); Vector2 infoPos = entityPos + new Vector2(128 * cam.Zoom, -(128 * cam.Zoom)); infoPos.Y -= infoSize.Y / 2; @@ -370,7 +370,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, infoRect, Color.White, isFilled: false); - GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextData, font: GUI.SmallFont); + GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextData, font: GUIStyle.SmallFont); GUI.DrawLine(spriteBatch, entityPos, new Vector2(infoRect.Location.X, infoRect.Location.Y + infoRect.Height / 2), Color.White); } @@ -489,15 +489,15 @@ namespace Barotrauma { text = text.TrimEnd('\r', '\n'); - string identifier = @event.Prefab.Identifier; - if (!string.IsNullOrWhiteSpace(identifier)) + Identifier identifier = @event.Prefab.Identifier; + if (!identifier.IsEmpty) { text = $"Identifier: {identifier.ColorizeObject()}\n{text}"; } - List richTextData = RichTextData.GetRichTextData(text, out text); + ImmutableArray? richTextData = RichTextData.GetRichTextData(text, out text); - Vector2 size = GUI.SmallFont.MeasureString(text); + Vector2 size = GUIStyle.SmallFont.MeasureString(text); Vector2 pos = pinnedPosition; Rectangle infoRect; Rectangle? infoBarRect = null; @@ -520,7 +520,7 @@ namespace Barotrauma const string titleHeader = "Pinned event"; GUI.DrawRectangle(spriteBatch, barRect, Color.DarkGray * 0.8f, isFilled: true); - GUI.DrawString(spriteBatch, barRect.Location.ToVector2() + barRect.Size.ToVector2() / 2 - GUI.SubHeadingFont.MeasureString(titleHeader) / 2, titleHeader, Color.White); + GUI.DrawString(spriteBatch, barRect.Location.ToVector2() + barRect.Size.ToVector2() / 2 - GUIStyle.SubHeadingFont.MeasureString(titleHeader) / 2, titleHeader, Color.White); GUI.DrawRectangle(spriteBatch, barRect, Color.White); infoBarRect = barRect; } @@ -546,8 +546,14 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, infoRect, Color.White); - GUI.DrawStringWithColors(spriteBatch, pos, text, Color.White, richTextData, null, 0, GUI.SmallFont); - richTextData.Clear(); + if (richTextData.HasValue && richTextData.Value.Length > 0) + { + GUI.DrawStringWithColors(spriteBatch, pos, text, Color.White, richTextData.Value, null, 0, GUIStyle.SmallFont); + } + else + { + GUI.DrawString(spriteBatch, pos, text, Color.White, null, 0, GUIStyle.SmallFont); + } return infoBarRect ?? infoRect; } @@ -557,7 +563,7 @@ namespace Barotrauma switch (eventType) { case NetworkEventType.STATUSEFFECT: - string eventIdentifier = msg.ReadString(); + Identifier eventIdentifier = msg.ReadIdentifier(); UInt16 actionIndex = msg.ReadUInt16(); UInt16 targetCount = msg.ReadUInt16(); List targets = new List(); @@ -571,14 +577,14 @@ namespace Barotrauma var eventPrefab = EventSet.GetEventPrefab(eventIdentifier); if (eventPrefab == null) { return; } int j = 0; - foreach (XElement element in eventPrefab.ConfigElement.Descendants()) + foreach (var element in eventPrefab.ConfigElement.Descendants()) { if (j != actionIndex) { j++; continue; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})"); @@ -633,13 +639,13 @@ namespace Barotrauma } break; case NetworkEventType.MISSION: - string missionIdentifier = msg.ReadString(); + Identifier missionIdentifier = msg.ReadIdentifier(); - MissionPrefab? prefab = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase)); + MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) { new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), - new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; @@ -657,7 +663,7 @@ namespace Barotrauma { GameMain.GameSession.Map.Connections[connectionIndex].Locked = false; new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"), - new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); + Array.Empty(), type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); } } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index 072e0615a..fcd12b072 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -13,7 +13,7 @@ namespace Barotrauma if (state != value) { base.State = value; - if (state == HostagesKilledState && !string.IsNullOrEmpty(hostagesKilledMessage)) + if (state == HostagesKilledState && !hostagesKilledMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, hostagesKilledMessage); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index e4de29441..08356c60c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -8,23 +8,29 @@ namespace Barotrauma public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; - public override string GetMissionRewardText(Submarine sub) + public override RichString GetMissionRewardText(Submarine sub) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); + LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); + LocalizedString retVal; if (rewardPerCrate.HasValue) { - string rewardPerCrateText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", rewardPerCrate.Value)); - return TextManager.GetWithVariables("missionrewardcargopercrate", - new string[] { "[rewardpercrate]", "[itemcount]", "[maxitemcount]", "[totalreward]" }, - new string[] { rewardPerCrateText, itemsToSpawn.Count.ToString(), maxItemCount.ToString(), $"‖color:gui.orange‖{rewardText}‖end‖" }); + LocalizedString rewardPerCrateText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", rewardPerCrate.Value)); + retVal = TextManager.GetWithVariables("missionrewardcargopercrate", + ("[rewardpercrate]", rewardPerCrateText), + ("[itemcount]", itemsToSpawn.Count.ToString()), + ("[maxitemcount]", maxItemCount.ToString()), + ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖")); } else { - return TextManager.GetWithVariables("missionrewardcargo", - new string[] { "[totalreward]", "[itemcount]", "[maxitemcount]" }, - new string[] { $"‖color:gui.orange‖{rewardText}‖end‖", itemsToSpawn.Count.ToString(), maxItemCount.ToString() }); + retVal = TextManager.GetWithVariables("missionrewardcargo", + ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖"), + ("[itemcount]", itemsToSpawn.Count.ToString()), + ("[maxitemcount]", maxItemCount.ToString())); } + + return RichString.Rich(retVal); } public override void ClientReadInitial(IReadMessage msg) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index 401f5278f..3957dff43 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -4,7 +4,7 @@ namespace Barotrauma { partial class CombatMission : Mission { - public override string Description + public override LocalizedString Description { get { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index f97da96d5..bb573b27d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -56,7 +56,7 @@ namespace Barotrauma for(int i = 0; i < resourceClusters.Count; i++) { - var identifier = msg.ReadString(); + var identifier = msg.ReadIdentifier(); var count = msg.ReadByte(); var resources = new Item[count]; for (int j = 0; j < count; j++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index ab7a3a6f0..cff6b76b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -9,11 +9,8 @@ namespace Barotrauma { abstract partial class Mission { - private readonly List shownMessages = new List(); - public IEnumerable ShownMessages - { - get { return shownMessages; } - } + private readonly List shownMessages = new List(); + public IEnumerable ShownMessages => shownMessages; public bool DisplayTargetHudIcons => Prefab.DisplayTargetHudIcons; @@ -32,29 +29,29 @@ namespace Barotrauma { int v = Difficulty ?? MissionPrefab.MinDifficulty; float t = MathUtils.InverseLerp(MissionPrefab.MinDifficulty, MissionPrefab.MaxDifficulty, v); - return ToolBox.GradientLerp(t, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } - public virtual string GetMissionRewardText(Submarine sub) + public virtual RichString GetMissionRewardText(Submarine sub) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - return TextManager.GetWithVariable("missionreward", "[reward]", $"‖color:gui.orange‖{rewardText}‖end‖"); + LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); + return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); } - public string GetReputationRewardText(Location currLocation) + public RichString GetReputationRewardText(Location currLocation) { - List reputationRewardTexts = new List(); + List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - string name = ""; + LocalizedString name = ""; - if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase)) + if (reputationReward.Key == "location") { name = $"‖color:gui.orange‖{currLocation.Name}‖end‖"; } else { - var faction = FactionPrefab.Prefabs.Find(f => f.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase)); + var faction = FactionPrefab.Prefabs.Find(f => f.Identifier == reputationReward.Key); if (faction != null) { name = $"‖color:{XMLExtensions.ColorToString(faction.IconColor)}‖{faction.Name}‖end‖"; @@ -66,28 +63,28 @@ namespace Barotrauma } float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); string formattedValue = ((int)reputationReward.Value).ToString("+#;-#;0"); //force plus sign for positive numbers - string rewardText = TextManager.GetWithVariables( + LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", - new string[] { "[reputationname]", "[reputationvalue]" }, - new string[] { name, $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" }); - reputationRewardTexts.Add(rewardText); + ("[reputationname]", name), + ("[reputationvalue]", $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); + reputationRewardTexts.Add(rewardText.Value); } - return TextManager.AddPunctuation(':', TextManager.Get("reputation"), string.Join(", ", reputationRewardTexts)); + return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); } partial void ShowMessageProjSpecific(int missionState) { int messageIndex = missionState - 1; - if (messageIndex >= Headers.Count && messageIndex >= Messages.Count) { return; } + if (messageIndex >= Headers.Length && messageIndex >= Messages.Length) { return; } if (messageIndex < 0) { return; } - string header = messageIndex < Headers.Count ? Headers[messageIndex] : ""; - string message = messageIndex < Messages.Count ? Messages[messageIndex] : ""; + LocalizedString header = messageIndex < Headers.Length ? Headers[messageIndex] : ""; + LocalizedString message = messageIndex < Messages.Length ? Messages[messageIndex] : ""; CoroutineManager.StartCoroutine(ShowMessageBoxAfterRoundSummary(header, message)); } - private IEnumerable ShowMessageBoxAfterRoundSummary(string header, string message) + private IEnumerable ShowMessageBoxAfterRoundSummary(LocalizedString header, LocalizedString message) { while (GUIMessageBox.VisibleBox?.UserData is RoundSummary) { @@ -97,10 +94,10 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected void CreateMessageBox(string header, string message) + protected void CreateMessageBox(LocalizedString header, LocalizedString message) { shownMessages.Add(message); - new GUIMessageBox(header, message, buttons: new string[0], type: GUIMessageBox.Type.InGame, icon: Prefab.Icon, parseRichText: true) + new GUIMessageBox(RichString.Rich(header), RichString.Rich(message), buttons: Array.Empty(), type: GUIMessageBox.Type.InGame, icon: Prefab.Icon) { IconColor = Prefab.IconColor }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 342b61004..3ec2386cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -1,4 +1,6 @@ -namespace Barotrauma +using System; + +namespace Barotrauma { abstract partial class MissionMode : GameMode { @@ -6,7 +8,7 @@ { foreach (Mission mission in missions) { - new GUIMessageBox(mission.Name, mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon, parseRichText: true) + new GUIMessageBox(RichString.Rich(mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, UserData = "missionstartmessage" diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index e3c6f8633..c8172bfaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -4,7 +4,7 @@ using System.Xml.Linq; namespace Barotrauma { - partial class MissionPrefab + partial class MissionPrefab : PrefabWithUintIdentifier { public Sprite Icon { @@ -49,11 +49,11 @@ namespace Barotrauma private Sprite hudIcon; private Color? hudIconColor; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string name = subElement.Name.ToString(); if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) @@ -68,5 +68,10 @@ namespace Barotrauma } } } + + partial void DisposeProjectSpecific() + { + Icon?.Remove(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index ad66f2a91..bbcc7acca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using SharpFont; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -19,7 +20,6 @@ namespace Barotrauma private Face face; private uint size; private int baseHeight; - //private int lineHeight; private Dictionary texCoords; private List textures; private GraphicsDevice graphicsDevice; @@ -53,6 +53,8 @@ namespace Barotrauma } } + public bool ForceUpperCase = false; + public float LineHeight => baseHeight * 1.8f; private uint[] charRanges; @@ -79,9 +81,9 @@ namespace Barotrauma } } - public ScalableFont(XElement element, GraphicsDevice gd = null) + public ScalableFont(ContentXElement element, GraphicsDevice gd = null) : this( - element.GetAttributeString("file", ""), + element.GetAttributeContentPath("file")?.Value, (uint)element.GetAttributeInt("size", 14), gd, element.GetAttributeBool("dynamicloading", false), @@ -104,10 +106,7 @@ namespace Barotrauma break; } } - if (this.face == null) - { - this.face = new Face(Lib, filename); - } + this.face ??= new Face(Lib, filename); this.size = size; this.textures = new List(); this.texCoords = new Dictionary(); @@ -186,7 +185,7 @@ namespace Barotrauma GlyphData blankData = new GlyphData( advance: (float)face.Glyph.Metrics.HorizontalAdvance, texIndex: -1); //indicates no texture because the glyph is empty - + texCoords.Add(j, blankData); } continue; @@ -399,6 +398,52 @@ namespace Barotrauma } } + // TODO: refactor this further + private void HandleNewLineAndAlignment( + string text, + in Vector2 advanceUnit, + in Vector2 position, + in Vector2 scale, + Alignment alignment, + int i, + ref float lineWidth, + ref Vector2 currentLineOffset, + ref int lineNum, + ref Vector2 currentPos, + out uint charIndex, + out bool shouldContinue) + { + if ((alignment.HasFlag(Alignment.CenterX) || alignment.HasFlag(Alignment.Right)) && (lineWidth < 0.0f || text[i] == '\n')) + { + int startIndex = lineWidth < 0.0f ? i : (i + 1); + lineWidth = 0.0f; + for (int j = startIndex; j < text.Length; j++) + { + if (text[j] == '\n') { break; } + uint chrIndex = text[j]; + + var gd2 = GetGlyphData(chrIndex); + lineWidth += gd2.Advance; + } + currentLineOffset = -lineWidth * advanceUnit * scale.X; + if (alignment.HasFlag(Alignment.CenterX)) { currentLineOffset *= 0.5f; } + + currentLineOffset.X = MathF.Round(currentLineOffset.X); + currentLineOffset.Y = MathF.Round(currentLineOffset.Y); + } + if (text[i] == '\n') + { + lineNum++; + currentPos = position; + currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; + currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; + shouldContinue = true; charIndex = 0; return; + } + + shouldContinue = false; + charIndex = text[i]; + } + private GlyphData GetGlyphData(uint charIndex) { const uint DEFAULT_INDEX = 0x25A1; //U+25A1 = white square @@ -412,29 +457,27 @@ namespace Barotrauma return new GlyphData(texIndex: -1); } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { if (textures.Count == 0 && !DynamicLoading) { return; } + text = ApplyUpperCase(text, forceUpperCase); if (DynamicLoading) { DynamicRenderAtlas(graphicsDevice, text); } + float lineWidth = -1.0f; + Vector2 currentLineOffset = Vector2.Zero; + int lineNum = 0; Vector2 currentPos = position; Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); for (int i = 0; i < text.Length; i++) { - if (text[i] == '\n') - { - lineNum++; - currentPos = position; - currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; - currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; - continue; - } - - uint charIndex = text[i]; + HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i, + ref lineWidth, ref currentLineOffset, ref lineNum, ref currentPos, + out uint charIndex, out bool shouldContinue); + if (shouldContinue) { continue; } GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) @@ -444,20 +487,30 @@ namespace Barotrauma drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X; - sb.Draw(tex, currentPos + drawOffset, gd.TexCoords, color, rotation, origin, scale, se, layerDepth); + sb.Draw(tex, currentPos + currentLineOffset + drawOffset, gd.TexCoords, color, rotation, origin, scale, se, layerDepth); } currentPos += gd.Advance * advanceUnit * scale.X; } } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth) + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - DrawString(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth); + DrawString(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, alignment, forceUpperCase); } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color) + private string ApplyUpperCase(string text, ForceUpperCase forceUpperCase) + => forceUpperCase switch + { + Barotrauma.ForceUpperCase.Inherit => ForceUpperCase ? text.ToUpper() : text, + Barotrauma.ForceUpperCase.Yes => text.ToUpper(), + Barotrauma.ForceUpperCase.No => text + }; + + private readonly static VertexPositionColorTexture[] quadVertices = new VertexPositionColorTexture[4]; + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) { if (textures.Count == 0 && !DynamicLoading) { return; } + text = ApplyUpperCase(text, forceUpperCase); if (DynamicLoading) { DynamicRenderAtlas(graphicsDevice, text); @@ -478,21 +531,48 @@ namespace Barotrauma GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) { + float halfCharHeight = gd.TexCoords.Height * 0.5f; + float slantStrength = 0.35f; + float topItalicOffset = italics ? ((halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f; + float bottomItalicOffset = italics ? ((-halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f; + Texture2D tex = textures[gd.TexIndex]; - sb.Draw(tex, currentPos + gd.DrawOffset, gd.TexCoords, color); + quadVertices[0].Position = new Vector3(currentPos + gd.DrawOffset + (bottomItalicOffset, gd.TexCoords.Height), 0.0f); + quadVertices[0].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Bottom / tex.Height); + quadVertices[0].Color = color; + + quadVertices[1].Position = new Vector3(currentPos + gd.DrawOffset + (topItalicOffset, 0.0f), 0.0f); + quadVertices[1].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Top / tex.Height); + quadVertices[1].Color = color; + + quadVertices[2].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + bottomItalicOffset, gd.TexCoords.Height), 0.0f); + quadVertices[2].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Bottom / tex.Height); + quadVertices[2].Color = color; + + quadVertices[3].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + topItalicOffset, 0.0f), 0.0f); + quadVertices[3].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Top / tex.Height); + quadVertices[3].Color = color; + + sb.Draw(tex, quadVertices, 0.0f); } currentPos.X += gd.Advance; } } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData, rtdOffset); + DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (!richTextData.HasValue || richTextData.Value.Length <= 0) { DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, forceUpperCase: forceUpperCase); return; } + + text = ApplyUpperCase(text, forceUpperCase); + + float lineWidth = -1.0f; + Vector2 currentLineOffset = Vector2.Zero; if (DynamicLoading) { DynamicRenderAtlas(graphicsDevice, text); @@ -503,27 +583,21 @@ namespace Barotrauma Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); int richTextDataIndex = 0; - RichTextData currentRichTextData = richTextData[richTextDataIndex]; + RichTextData currentRichTextData = richTextData.Value[richTextDataIndex]; for (int i = 0; i < text.Length; i++) { - if (text[i] == '\n') - { - lineNum++; - currentPos = position; - currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; - currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; - continue; - } - - uint charIndex = text[i]; + HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i, + ref lineWidth, ref currentLineOffset, ref lineNum, ref currentPos, + out uint charIndex, out bool shouldContinue); + if (shouldContinue) { continue; } Color currentTextColor; while (currentRichTextData != null && i + rtdOffset > currentRichTextData.EndIndex + lineNum) { richTextDataIndex++; - currentRichTextData = richTextDataIndex < richTextData.Count ? richTextData[richTextDataIndex] : null; + currentRichTextData = richTextDataIndex < richTextData.Value.Length ? richTextData.Value[richTextDataIndex] : null; } if (currentRichTextData != null && currentRichTextData.StartIndex + lineNum <= i + rtdOffset && i + rtdOffset <= currentRichTextData.EndIndex + lineNum) @@ -547,7 +621,7 @@ namespace Barotrauma drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X; - sb.Draw(tex, currentPos + drawOffset, gd.TexCoords, currentTextColor, rotation, origin, scale, se, layerDepth); + sb.Draw(tex, currentPos + currentLineOffset + drawOffset, gd.TexCoords, currentTextColor, rotation, origin, scale, se, layerDepth); } currentPos += gd.Advance * advanceUnit * scale.X; } @@ -628,6 +702,8 @@ namespace Barotrauma //A breaker (whitespace or CJK) was found earlier //in this line, so let's break the line there i = lastBreakerIndex.Value + 1; + gd = GetGlyphData(text[i]); + advance = gd.Advance; } nextLine(); @@ -648,7 +724,12 @@ namespace Barotrauma requestedCharPos = foundCharPos; return result; } - + + public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false) + { + return MeasureString(str.Value, removeExtraSpacing); + } + public Vector2 MeasureString(string text, bool removeExtraSpacing = false) { if (text == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 6e3ae862d..35ae05d0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -25,7 +25,7 @@ namespace Barotrauma get { return _toggleOpen; } set { - _toggleOpen = GameMain.Config.ChatOpen = value; + _toggleOpen = value; if (value) hideableElements.Visible = true; } } @@ -121,7 +121,7 @@ namespace Barotrauma }; arrowIcon.HoverColor = arrowIcon.PressedColor = arrowIcon.PressedColor = arrowIcon.Color; - channelText = new GUITextBox(new RectTransform(new Vector2(0.25f, 0.8f), channelSettingsContent.RectTransform), style: "DigitalFrameLight", textAlignment: Alignment.Center, font: GUI.DigitalFont) + channelText = new GUITextBox(new RectTransform(new Vector2(0.25f, 0.8f), channelSettingsContent.RectTransform), style: "DigitalFrameLight", textAlignment: Alignment.Center, font: GUIStyle.DigitalFont) { textFilterFunction = text => { @@ -173,7 +173,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), channelPickerContent.RectTransform), i.ToString(), style: "GUITextBlock") { TextColor = new Color(51, 59, 46), - SelectedTextColor = GUI.Style.Green, + SelectedTextColor = GUIStyle.Green, UserData = i, OnClicked = (btn, userdata) => { @@ -185,13 +185,13 @@ namespace Barotrauma int.TryParse(channelText.Text, out int newChannel); radio.SetChannelMemory(index, newChannel); btn.ToolTip = TextManager.GetWithVariables("radiochannelpreset", - new string[] { "[index]", "[channel]" }, - new string[] { index.ToString(), radio.GetChannelMemory(index).ToString() }); + ("[index]", index.ToString()), + ("[channel]", radio.GetChannelMemory(index).ToString())); channelMemPending = false; channelPickerContent.Children.First().CanBeFocused = true; memButton.Enabled = true; - channelPickerContent.Flash(GUI.Style.Green); - channelText.Flash(GUI.Style.Green); + channelPickerContent.Flash(GUIStyle.Green); + channelText.Flash(GUIStyle.Green); } SetChannel(radio.GetChannelMemory(index), setText: true); SoundPlayer.PlayUISound(GUISoundType.PopupMenu); @@ -224,7 +224,7 @@ namespace Barotrauma style: "ChatTextBox") { OverflowClip = true, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, MaxTextLength = ChatMessage.MaxLength }; @@ -265,7 +265,7 @@ namespace Barotrauma }; showNewMessagesButton.Visible = false; - ToggleOpen = GameMain.Config.ChatOpen; + ToggleOpen = GameSettings.CurrentConfig.ChatOpen; } public bool TypingChatMessage(GUITextBox textBox, string text) @@ -337,7 +337,7 @@ namespace Barotrauma color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f); GUITextBlock senderNameTimestamp = new GUITextBlock(new RectTransform(new Vector2(0.98f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(5 * GUI.Scale), 0) }, - ChatMessage.GetTimeStamp(), textColor: Color.LightGray, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null) + ChatMessage.GetTimeStamp(), textColor: Color.LightGray, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft, style: null) { CanBeFocused = true }; @@ -350,9 +350,9 @@ namespace Barotrauma { Padding = Vector4.Zero }, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, CanBeFocused = true, - ForceUpperCase = false, + ForceUpperCase = ForceUpperCase.No, UserData = message.SenderClient, OnClicked = (_, o) => { @@ -379,8 +379,8 @@ namespace Barotrauma var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameTimestamp == null ? 0 : senderNameTimestamp.Rect.Height) }, - displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, - color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, parseRichText: true) + RichString.Rich(displayedText), textColor: message.Color, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, + color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f) { UserData = message.SenderName, CanBeFocused = false @@ -454,7 +454,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(senderName)) { var senderText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - senderName, textColor: senderColor, style: null, font: GUI.SmallFont) + senderName, textColor: senderColor, style: null, font: GUIStyle.SmallFont) { CanBeFocused = false }; @@ -462,7 +462,7 @@ namespace Barotrauma senderText.RectTransform.MinSize = new Point(0, senderText.Rect.Height); } var msgPopupText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.BottomLeft, style: null, wrap: true, parseRichText: true) + RichString.Rich(displayedText), textColor: message.Color, font: GUIStyle.SmallFont, textAlignment: Alignment.BottomLeft, style: null, wrap: true) { CanBeFocused = false }; @@ -553,8 +553,8 @@ namespace Barotrauma { int index = (int)presetButton.UserData; presetButton.ToolTip = TextManager.GetWithVariables("radiochannelpreset", - new string[] { "[index]", "[channel]" }, - new string[] { index.ToString(), radio.GetChannelMemory(index).ToString() }); + ("[index]", index.ToString()), + ("[channel]", radio.GetChannelMemory(index).ToString())); } SetChannel(radio.Channel, setText: true); prevRadio = radio; @@ -563,7 +563,7 @@ namespace Barotrauma { if (channelPickerContent.FlashTimer <= 0) { - channelPickerContent.Flash(GUI.Style.Green, flashRectInflate: new Vector2(GUI.Scale * 5.0f)); + channelPickerContent.Flash(GUIStyle.Green, flashRectInflate: new Vector2(GUI.Scale * 5.0f)); } if (PlayerInput.PrimaryMouseButtonClicked() && !GUI.IsMouseOn(channelPickerContent)) { @@ -671,7 +671,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { radio.Channel = channel; - GameMain.Client?.CreateEntityEvent(radio.Item, new object[] { NetEntityEvent.Type.ChangeProperty, radio.SerializableProperties["channel"] }); + GameMain.Client?.CreateEntityEvent(radio.Item, new object[] { NetEntityEvent.Type.ChangeProperty, radio.SerializableProperties["channel".ToIdentifier()] }); if (setText) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 07e7ef7f8..7d3b239de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -16,7 +17,7 @@ namespace Barotrauma Toggle } - public class GUIComponentStyle + public class GUIComponentStyle : GUIPrefab { public readonly Vector4 Padding; @@ -35,31 +36,53 @@ namespace Barotrauma public readonly float ColorCrossFadeTime; public readonly TransitionMode TransitionMode; - public readonly string Font; + public readonly Identifier Font; public readonly bool ForceUpperCase; public readonly Color OutlineColor; - public readonly XElement Element; + public readonly ContentXElement Element; public readonly Dictionary> Sprites; public SpriteFallBackState FallBackState; - - public Dictionary ChildStyles; - public readonly GUIStyle Style; + public readonly GUIComponentStyle ParentStyle; + public readonly Dictionary ChildStyles; + + public static GUIComponentStyle FromHierarchy(IReadOnlyList hierarchy) + { + if (hierarchy is null || hierarchy.None()) { return null; } + GUIStyle.ComponentStyles.TryGet(hierarchy[0], out GUIComponentStyle style); + for (int i = 1; i < hierarchy.Count; i++) + { + if (style is null) { return null; } + style.ChildStyles.TryGetValue(hierarchy[i], out style); + } + return style; + } + + public static Identifier[] ToHierarchy(GUIComponentStyle style) + { + List ids = new List(); + while (style != null) + { + ids.Insert(0, style.Identifier); + style = style.ParentStyle; + } + + return ids.ToArray(); + } public readonly string Name; public int? Width { get; private set; } public int? Height { get; private set; } - public GUIComponentStyle(XElement element, GUIStyle style) + public GUIComponentStyle(ContentXElement element, UIStyleFile file, GUIComponentStyle parent = null) : base(element, file) { Name = element.Name.LocalName; - Style = style; Element = element; Sprites = new Dictionary>(); @@ -68,7 +91,8 @@ namespace Barotrauma Sprites[state] = new List(); } - ChildStyles = new Dictionary(); + ParentStyle = parent; + ChildStyles = new Dictionary(); Padding = element.GetAttributeVector4("padding", Vector4.Zero); @@ -95,10 +119,10 @@ namespace Barotrauma FallBackState = s; } - Font = element.GetAttributeString("font", ""); + Font = element.GetAttributeIdentifier("font", ""); ForceUpperCase = element.GetAttributeBool("forceuppercase", false); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -128,15 +152,15 @@ namespace Barotrauma case "size": break; default: - string styleName = subElement.Name.ToString().ToLowerInvariant(); + Identifier styleName = subElement.NameAsIdentifier(); if (ChildStyles.ContainsKey(styleName)) { DebugConsole.ThrowError("UI style \"" + element.Name.ToString() + "\" contains multiple child styles with the same name (\"" + styleName + "\")!"); - ChildStyles[styleName] = new GUIComponentStyle(subElement, style); + ChildStyles[styleName] = new GUIComponentStyle(subElement, file, this); } else { - ChildStyles.Add(styleName, new GUIComponentStyle(subElement, style)); + ChildStyles.Add(styleName, new GUIComponentStyle(subElement, file, this)); } break; } @@ -157,7 +181,7 @@ namespace Barotrauma public void GetSize(XElement element) { Point size = new Point(0, 0); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); @@ -172,5 +196,7 @@ namespace Barotrauma if (size.X > 0) { Width = size.X; } if (size.Y > 0) { Height = size.Y; } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 9e975d413..32cfeee49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -108,10 +108,10 @@ namespace Barotrauma }; var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "CrewManagementHeaderIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaigncrew.header"), font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaigncrew.header"), font: GUIStyle.LargeFont) { CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; var hireablesGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, @@ -162,13 +162,13 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - TextManager.Get("campaignstore.balance"), font: GUI.Font, textAlignment: Alignment.BottomRight) + TextManager.Get("campaignstore.balance"), font: GUIStyle.Font, textAlignment: Alignment.BottomRight) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopRight) { AutoScaleVertical = true, TextScale = 1.1f, @@ -182,13 +182,13 @@ namespace Barotrauma }).RectTransform)); float height = 0.05f; - new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont); pendingList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont); crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 @@ -196,7 +196,7 @@ namespace Barotrauma var group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), TextManager.Get("campaignstore.total")); - totalBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + totalBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextScale = 1.1f }; @@ -207,12 +207,12 @@ namespace Barotrauma validateHiresButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) { ClickSound = GUISoundType.HireRepairClick, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, OnClicked = (b, o) => ValidateHires(PendingHires, true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, Enabled = HasPermission, OnClicked = (b, o) => RemoveAllPendingHires() }; @@ -302,30 +302,42 @@ namespace Barotrauma if (sortingMethod == SortingMethod.AlphabeticalAsc) { list.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as Tuple).Item1.Name.CompareTo((y.GUIComponent.UserData as Tuple).Item1.Name)); + ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name)); } else if (sortingMethod == SortingMethod.JobAsc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - String.Compare((x.GUIComponent.UserData as Tuple)?.Item1.Job.Name, (y.GUIComponent.UserData as Tuple).Item1.Job.Name, StringComparison.Ordinal)); + String.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); } else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as Tuple).Item1.Salary.CompareTo((y.GUIComponent.UserData as Tuple).Item1.Salary)); + ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary)); if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } } else if (sortingMethod == SortingMethod.SkillAsc || sortingMethod == SortingMethod.SkillDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as Tuple).Item2.CompareTo((y.GUIComponent.UserData as Tuple).Item2)); + ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel)); if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); } } } + private readonly struct InfoSkill + { + public readonly CharacterInfo CharacterInfo; + public readonly float SkillLevel; + + public InfoSkill(CharacterInfo characterInfo, float skillLevel) + { + CharacterInfo = characterInfo; + SkillLevel = skillLevel; + } + } + private void CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox) { Skill skill = null; @@ -338,7 +350,7 @@ namespace Barotrauma GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 55)), parent: listBox.Content.RectTransform), "ListBoxElement") { - UserData = new Tuple(characterInfo, skill?.Level ?? 0.0f) + UserData = new InfoSkill(characterInfo, skill?.Level ?? 0.0f) }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, anchor: Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { @@ -363,7 +375,7 @@ namespace Barotrauma nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), - characterInfo.Job.Name, textColor: Color.White, font: GUI.SmallFont, textAlignment: Alignment.TopLeft) + characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) { CanBeFocused = false }; @@ -374,7 +386,7 @@ namespace Barotrauma { GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true); float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width; - GUIImage skillIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1.0f), skillGroup.RectTransform), skill.Icon) + GUIImage skillIcon = new GUIImage(new RectTransform(Vector2.One, skillGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), skill.Icon, scaleToFit: true) { CanBeFocused = false }; @@ -448,7 +460,7 @@ namespace Barotrauma var confirmDialog = new GUIMessageBox( TextManager.Get("FireWarningHeader"), TextManager.GetWithVariable("FireWarningText", "[charactername]", ((CharacterInfo)obj).Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); confirmDialog.Buttons[0].UserData = (CharacterInfo)obj; confirmDialog.Buttons[0].OnClicked = FireCharacter; confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; @@ -510,10 +522,11 @@ namespace Barotrauma string name = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name; nameBlock.Text = ToolBox.LimitString(name, nameBlock.Font, nameBlock.Rect.Width); - if (characterInfo.HasGenders) + if (characterInfo.HasSpecifierTags) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("gender")); - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.Gender.ToString())); + var menuCategoryVar = characterInfo.Prefab.MenuCategoryVar; + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get(menuCategoryVar)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.ReplaceVars($"[{menuCategoryVar}]"))); } if (characterInfo.Job is Job job) { @@ -523,7 +536,7 @@ namespace Barotrauma if (characterInfo.PersonalityTrait is NPCPersonalityTrait trait) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait")); - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get("personalitytrait." + trait.Name.Replace(" ", ""))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get("personalitytrait." + trait.Name.Replace(" ".ToIdentifier(), Identifier.Empty))); } infoLabelGroup.Recalculate(); infoValueGroup.Recalculate(); @@ -568,7 +581,7 @@ namespace Barotrauma return false; } - hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo)); + hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo)); hireableList.UpdateScrollBarSize(); if (!PendingHires.Contains(characterInfo)) { PendingHires.Add(characterInfo); } CreateCharacterFrame(characterInfo, pendingList); @@ -582,14 +595,14 @@ namespace Barotrauma private bool RemovePendingHire(CharacterInfo characterInfo, bool setTotalHireCost = true, bool createNetworkMessage = true) { if (PendingHires.Contains(characterInfo)) { PendingHires.Remove(characterInfo); } - pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo)); + pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo)); pendingList.UpdateScrollBarSize(); // Server will reset the names to originals in multiplayer if (!GameMain.IsMultiplayer) { characterInfo?.ResetName(); } if (campaign.Map.CurrentLocation.HireManager.AvailableCharacters.Any(info => info.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()) && - hireableList.Content.Children.None(c => c.UserData is Tuple userData && userData.Item1.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName())) + hireableList.Content.Children.None(c => c.UserData is InfoSkill userData && userData.CharacterInfo.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName())) { CreateCharacterFrame(characterInfo, hireableList); SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); @@ -603,7 +616,7 @@ namespace Barotrauma private bool RemoveAllPendingHires(bool createNetworkMessage = true) { - pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire((c.UserData as Tuple).Item1, setTotalHireCost: false, createNetworkMessage)); + pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire(((InfoSkill)c.UserData).CharacterInfo, setTotalHireCost: false, createNetworkMessage)); SetTotalHireCost(); return true; } @@ -614,7 +627,7 @@ namespace Barotrauma int total = 0; pendingList.Content.Children.ForEach(c => { - total += (c.UserData as Tuple).Item1.Salary; + total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); totalBlock.Text = FormatCurrency(total); bool enoughMoney = campaign != null ? total <= campaign.Money : true; @@ -661,7 +674,7 @@ namespace Barotrauma var dialog = new GUIMessageBox( TextManager.Get("newcrewmembers"), TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), - new string[] { TextManager.Get("Ok") }); + new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; } @@ -687,7 +700,7 @@ namespace Barotrauma RelativeSpacing = 0.02f, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); var groupElementSize = new Vector2(1.0f, 0.25f); var nameBox = new GUITextBox(new RectTransform(groupElementSize, layoutGroup.RectTransform)) { @@ -732,7 +745,7 @@ namespace Barotrauma } else { - var crewComponent = crewList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo); + var crewComponent = crewList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo); if (crewComponent != null) { crewList.Content.RemoveChild(crewComponent); @@ -742,7 +755,7 @@ namespace Barotrauma } else { - var pendingComponent = pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo); + var pendingComponent = pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo); if (pendingComponent != null) { pendingList.Content.RemoveChild(pendingComponent); @@ -821,15 +834,15 @@ namespace Barotrauma characterPreviewFrame = null; } - static (GUIComponent, CharacterInfo) FindHighlightedCharacter(GUIComponent c) + static (GUIComponent GuiComponent, CharacterInfo CharacterInfo) FindHighlightedCharacter(GUIComponent c) { if (c == null) { return default; } - if (c.UserData is Tuple highlightedData) + if (c.UserData is InfoSkill highlightedData) { - return (c, highlightedData.Item1); + return (c, highlightedData.CharacterInfo); } if (c.Parent != null) { @@ -913,6 +926,6 @@ namespace Barotrauma } } - private string FormatCurrency(int currency) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", currency)); + private LocalizedString FormatCurrency(int currency) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", currency)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 0afc20985..2f54a3d4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -1,9 +1,11 @@ -using Microsoft.Xna.Framework; +#nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Text; +using Barotrauma.Extensions; namespace Barotrauma { @@ -18,7 +20,7 @@ namespace Barotrauma } set { - if (value && backgroundFrame == null) { Init(); } + if (value) { InitIfNecessary(); } if (!value) { fileSystemWatcher?.Dispose(); @@ -28,26 +30,31 @@ namespace Barotrauma } } - private static GUIFrame backgroundFrame; - private static GUIFrame window; - private static GUIListBox sidebar; - private static GUIListBox fileList; - private static GUITextBox directoryBox; - private static GUITextBox filterBox; - private static GUITextBox fileBox; - private static GUIDropDown fileTypeDropdown; - private static GUIButton openButton; + private static GUIFrame? backgroundFrame; + private static GUIFrame? window; + private static GUIListBox? sidebar; + private static GUIListBox? fileList; + private static GUITextBox? directoryBox; + private static GUITextBox? filterBox; + private static GUITextBox? fileBox; + private static GUIDropDown? fileTypeDropdown; + private static GUIButton? openButton; - private static System.IO.FileSystemWatcher fileSystemWatcher; + private static System.IO.FileSystemWatcher? fileSystemWatcher; - private static string currentFileTypePattern; + private enum ItemIsDirectory + { + Yes, No + } - private static readonly string[] ignoredDrivePrefixes = new string[] + private static string? currentFileTypePattern; + + private static readonly string[] ignoredDrivePrefixes = { "/sys/", "/snap/" }; - private static string currentDirectory; + private static string currentDirectory = ""; public static string CurrentDirectory { get @@ -91,7 +98,7 @@ namespace Barotrauma } } - public static Action OnFileSelected + public static Action? OnFileSelected { get; set; @@ -99,15 +106,16 @@ namespace Barotrauma private static void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) { + if (fileList is null) { return; } switch (e.ChangeType) { case System.IO.WatcherChangeTypes.Created: { - var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), e.Name) + var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), e.Name ?? string.Empty) { - UserData = (bool?)Directory.Exists(e.FullPath) + UserData = Directory.Exists(e.FullPath) ? ItemIsDirectory.Yes : ItemIsDirectory.No }; - if ((itemFrame.UserData as bool?) ?? false) + if (itemFrame.UserData is ItemIsDirectory.Yes) { itemFrame.Text += "/"; } @@ -122,11 +130,13 @@ namespace Barotrauma break; case System.IO.WatcherChangeTypes.Renamed: { - System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs; - var itemFrame = fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == renameArgs.OldName || tb.Text == renameArgs.OldName + "/")) as GUITextBlock; - itemFrame.UserData = (bool?)Directory.Exists(e.FullPath); - itemFrame.Text = renameArgs.Name; - if ((itemFrame.UserData as bool?) ?? false) + System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs ?? throw new InvalidCastException($"Unable to cast {nameof(System.IO.FileSystemEventArgs)} to {nameof(System.IO.RenamedEventArgs)}."); + var itemFrame = + fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == renameArgs.OldName || tb.Text == renameArgs.OldName + "/")) as GUITextBlock + ?? throw new Exception($"Could not find file list item with name \"{renameArgs.OldName}\""); + itemFrame.UserData = Directory.Exists(e.FullPath) ? ItemIsDirectory.Yes : ItemIsDirectory.No; + itemFrame.Text = renameArgs.Name ?? string.Empty; + if (itemFrame.UserData is ItemIsDirectory.Yes) { itemFrame.Text += "/"; } @@ -138,10 +148,10 @@ namespace Barotrauma private static int SortFiles(RectTransform r1, RectTransform r2) { - string file1 = (r1.GUIComponent as GUITextBlock)?.Text ?? ""; - string file2 = (r2.GUIComponent as GUITextBlock)?.Text ?? ""; - bool dir1 = (r1.GUIComponent.UserData as bool?) ?? false; - bool dir2 = (r2.GUIComponent.UserData as bool?) ?? false; + string file1 = (r1.GUIComponent as GUITextBlock)?.Text?.SanitizedValue ?? ""; + string file2 = (r2.GUIComponent as GUITextBlock)?.Text?.SanitizedValue ?? ""; + bool dir1 = r1.GUIComponent.UserData is ItemIsDirectory.Yes; + bool dir2 = r2.GUIComponent.UserData is ItemIsDirectory.Yes; if (dir1 && !dir2) { return -1; @@ -154,6 +164,11 @@ namespace Barotrauma return string.Compare(file1, file2); } + private static void InitIfNecessary() + { + if (backgroundFrame == null) { Init(); } + } + public static void Init() { backgroundFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) @@ -179,7 +194,7 @@ namespace Barotrauma sidebar.OnSelected = (child, userdata) => { - CurrentDirectory = (child as GUITextBlock).Text; + CurrentDirectory = (child as GUITextBlock)?.Text.SanitizedValue ?? throw new Exception("Sidebar selection is invalid"); return false; }; @@ -228,13 +243,14 @@ namespace Barotrauma { OnSelected = (child, userdata) => { - if (userdata == null) { return false; } + if (userdata is null) { return false; } + if (fileBox is null) { return false; } - var fileName = (child as GUITextBlock).Text; + var fileName = (child as GUITextBlock)!.Text.SanitizedValue; fileBox.Text = fileName; if (PlayerInput.DoubleClicked()) { - bool isDir = (userdata as bool?).Value; + bool isDir = userdata is ItemIsDirectory.Yes; if (isDir) { CurrentDirectory += fileName; @@ -263,7 +279,7 @@ namespace Barotrauma { OnSelected = (child, userdata) => { - currentFileTypePattern = (child as GUITextBlock).UserData as string; + currentFileTypePattern = (child as GUITextBlock)!.UserData as string; RefreshFileList(); return true; @@ -307,30 +323,31 @@ namespace Barotrauma public static void ClearFileTypeFilters() { - if (backgroundFrame == null) { Init(); } - fileTypeDropdown.ClearChildren(); + InitIfNecessary(); + fileTypeDropdown!.ClearChildren(); } public static void AddFileTypeFilter(string name, string pattern) { - if (backgroundFrame == null) { Init(); } - fileTypeDropdown.AddItem(name + " (" + pattern + ")", pattern); + InitIfNecessary(); + fileTypeDropdown!.AddItem(name + " (" + pattern + ")", pattern); } public static void SelectFileTypeFilter(string pattern) { - if (backgroundFrame == null) { Init(); } - fileTypeDropdown.SelectItem(pattern); + InitIfNecessary(); + fileTypeDropdown!.SelectItem(pattern); } public static void RefreshFileList() { - fileList.Content.ClearChildren(); + InitIfNecessary(); + fileList!.Content.ClearChildren(); fileList.BarScroll = 0.0f; try { - var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox.Text + "*"); + var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox!.Text + "*"); foreach (var directory in directories) { string txt = directory; @@ -338,7 +355,7 @@ namespace Barotrauma if (!txt.EndsWith("/")) { txt += "/"; } var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) { - UserData = (bool?)true + UserData = ItemIsDirectory.Yes }; var folderIcon = new GUIImage(new RectTransform(new Point((int)(itemFrame.Rect.Height * 0.8f)), itemFrame.RectTransform, Anchor.CenterLeft) { @@ -347,18 +364,18 @@ namespace Barotrauma itemFrame.Padding = new Vector4(folderIcon.Rect.Width * 1.5f, itemFrame.Padding.Y, itemFrame.Padding.Z, itemFrame.Padding.W); } - IEnumerable files = null; - if (currentFileTypePattern == null) + IEnumerable files = Enumerable.Empty(); + if (currentFileTypePattern.IsNullOrEmpty()) { files = Directory.GetFiles(currentDirectory); } else { - foreach (string pattern in currentFileTypePattern.Split(',')) + foreach (string pattern in currentFileTypePattern!.Split(',')) { string patternTrimmed = pattern.Trim(); patternTrimmed = "*" + filterBox.Text + "*" + patternTrimmed; - if (files == null) + if (files.None()) { files = Directory.EnumerateFiles(currentDirectory, patternTrimmed); } @@ -375,7 +392,7 @@ namespace Barotrauma if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); } var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) { - UserData = (bool?)false + UserData = ItemIsDirectory.No }; } } @@ -387,8 +404,8 @@ namespace Barotrauma }; } - directoryBox.Text = currentDirectory; - fileBox.Text = ""; + directoryBox!.Text = currentDirectory; + fileBox!.Text = ""; fileList.Deselect(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 116f2b857..c83bca84c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -14,6 +14,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; +using System.Collections.Immutable; namespace Barotrauma { @@ -36,13 +37,13 @@ namespace Barotrauma public enum CursorState { - Default, // Cursor - Hand, // Hand with a finger - Move, // arrows pointing to all directions - IBeam, // Text - Dragging,// Closed hand - Waiting, // Hourglass - WaitingBackground // Cursor + Hourglass + Default = 0, // Cursor + Hand = 1, // Hand with a finger + Move = 2, // arrows pointing to all directions + IBeam = 3, // Text + Dragging = 4,// Closed hand + Waiting = 5, // Hourglass + WaitingBackground = 6, // Cursor + Hourglass } public static class GUI @@ -78,20 +79,19 @@ namespace Barotrauma FilterMode = TextureFilterMode.Default, }; - - public static readonly string[] vectorComponentLabels = { "X", "Y", "Z", "W" }; - public static readonly string[] rectComponentLabels = { "X", "Y", "W", "H" }; - public static readonly string[] colorComponentLabels = { "R", "G", "B", "A" }; + public static readonly string[] VectorComponentLabels = { "X", "Y", "Z", "W" }; + public static readonly string[] RectComponentLabels = { "X", "Y", "W", "H" }; + public static readonly string[] ColorComponentLabels = { "R", "G", "B", "A" }; private static readonly object mutex = new object(); - public static Vector2 ReferenceResolution => new Vector2(1920f, 1080f); - public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.HUDScale; - public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.HUDScale; - public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.HUDScale; + public static readonly Vector2 ReferenceResolution = new Vector2(1920f, 1080f); + public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.CurrentConfig.Graphics.HUDScale; + public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.CurrentConfig.Graphics.HUDScale; + public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.CurrentConfig.Graphics.HUDScale; public static int IntScale(float f) => (int)(f * Scale); public static int IntScaleFloor(float f) => (int)Math.Floor(f * Scale); - public static int IntScaleCeiling(float f) => (int) Math.Ceiling(f * Scale); + public static int IntScaleCeiling(float f) => (int)Math.Ceiling(f * Scale); public static float HorizontalAspectRatio => GameMain.GraphicsWidth / (float)GameMain.GraphicsHeight; public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); @@ -102,7 +102,6 @@ namespace Barotrauma { get { - // Ultrawide if (IsUltrawide) { return (int)(GameMain.GraphicsHeight * ReferenceResolution.X / ReferenceResolution.Y); @@ -127,23 +126,21 @@ namespace Barotrauma } } - public static GUIStyle Style; - - private static Texture2D t; - public static Texture2D WhiteTexture => t; - private static Sprite[] MouseCursorSprites => Style.CursorSprite; + private static Texture2D solidWhiteTexture; + public static Texture2D WhiteTexture => solidWhiteTexture; + private static GUICursor MouseCursorSprites => GUIStyle.CursorSprite; private static bool debugDrawSounds, debugDrawEvents, debugDrawMetadata; private static int debugDrawMetadataOffset; private static readonly string[] ignoredMetadataInfo = { string.Empty, string.Empty, string.Empty, string.Empty }; - public static GraphicsDevice GraphicsDevice { get; private set; } + public static GraphicsDevice GraphicsDevice => GameMain.Instance.GraphicsDevice; private static List messages = new List(); - private static readonly Dictionary soundIdentifiers = new Dictionary(); - private static bool pauseMenuOpen, settingsMenuOpen; + public static GUIFrame PauseMenu { get; private set; } - private static Sprite arrow; + public static GUIFrame SettingsMenuContainer { get; private set; } + public static Sprite Arrow => GUIStyle.Arrow.Value.Sprite; public static bool HideCursor; @@ -154,61 +151,31 @@ namespace Barotrauma /// public static bool ScreenChanged; - public static ScalableFont Font => Style?.Font; - - // Usable in CJK as a regular font - public static ScalableFont GlobalFont => Style?.GlobalFont; - public static ScalableFont UnscaledSmallFont => Style?.UnscaledSmallFont; - public static ScalableFont SmallFont => Style?.SmallFont; - public static ScalableFont LargeFont => Style?.LargeFont; - public static ScalableFont SubHeadingFont => Style?.SubHeadingFont; - public static ScalableFont DigitalFont => Style?.DigitalFont; - public static ScalableFont HotkeyFont => Style?.HotkeyFont; - public static ScalableFont MonospacedFont => Style?.MonospacedFont; - - public static ScalableFont CJKFont { get; private set; } - - public static UISprite UIGlow => Style.UIGlow; - public static UISprite UIGlowCircular => Style.UIGlowCircular; - - public static Sprite SubmarineIcon - { - get; - private set; - } - - public static Sprite BrokenIcon - { - get; - private set; - } - - public static Sprite SpeechBubbleIcon - { - get; - private set; - } - - public static Sprite Arrow - { - get { return arrow; } - } - + private static bool settingsMenuOpen; public static bool SettingsMenuOpen { get { return settingsMenuOpen; } set { - if (value == settingsMenuOpen) { return; } - GameMain.Config.ResetSettingsFrame(); + if (value == SettingsMenuOpen) { return; } + + if (value) + { + SettingsMenuContainer = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, SettingsMenuContainer.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + var settingsMenuInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), SettingsMenuContainer.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(640, 480) }); + SettingsMenu.Create(settingsMenuInner.RectTransform); + } + else + { + SettingsMenu.Instance?.Close(); + } settingsMenuOpen = value; } } - public static bool PauseMenuOpen - { - get { return pauseMenuOpen; } - } + public static bool PauseMenuOpen { get; private set; } public static bool InputBlockingMenuOpen { @@ -251,66 +218,14 @@ namespace Barotrauma FadingOut } - public static void Init(GameWindow window, IEnumerable selectedContentPackages, GraphicsDevice graphicsDevice) + public static void Init() { - GraphicsDevice = graphicsDevice; - - var files = ContentPackage.GetFilesOfType(selectedContentPackages, ContentType.UIStyle); - XElement selectedStyle = null; - foreach (var file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - if (selectedStyle != null) - { - DebugConsole.NewMessage($"Overriding the ui styles with '{file.Path}'", Color.Yellow); - } - } - else if (selectedStyle != null) - { - DebugConsole.ThrowError("Another ui style already loaded! Use tags to override it."); - break; - } - selectedStyle = mainElement; - } - if (selectedStyle == null) - { - DebugConsole.ThrowError("No UI styles defined in the selected content package!"); - } - else - { - Style = new GUIStyle(selectedStyle, graphicsDevice); - } - - if (CJKFont == null) - { - CJKFont = new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", - Font.Size, graphicsDevice, dynamicLoading: true, isCJK: true); - } - } - - public static void LoadContent() - { - foreach (GUISoundType soundType in Enum.GetValues(typeof(GUISoundType))) - { - soundIdentifiers.Add(soundType, soundType.ToString().ToLowerInvariant()); - } - // create 1x1 texture for line drawing CrossThread.RequestExecutionOnMainThread(() => { - t = new Texture2D(GraphicsDevice, 1, 1); - t.SetData(new Color[] { Color.White });// fill the texture with white + solidWhiteTexture = new Texture2D(GraphicsDevice, 1, 1); + solidWhiteTexture.SetData(new Color[] { Color.White });// fill the texture with white }); - - SubmarineIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(452, 385, 182, 81), new Vector2(0.5f, 0.5f)); - arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(393, 393, 49, 45), new Vector2(0.5f, 0.5f)); - SpeechBubbleIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(385, 449, 66, 60), new Vector2(0.5f, 0.5f)); - BrokenIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(898, 386, 123, 123), new Vector2(0.5f, 0.5f)); } /// @@ -341,41 +256,41 @@ namespace Barotrauma } #if UNSTABLE - string line1 = "Barotrauma Unstable v" + GameMain.Version; - string line2 = "(" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; + string line1 = "Barotrauma Unstable v" + GameMain.Version; + string line2 = "(" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; - Rectangle watermarkRect = new Rectangle(-50, GameMain.GraphicsHeight - 80, 50 + (int)(Math.Max(LargeFont.MeasureString(line1).X, Font.MeasureString(line2).X) * 1.2f), 100); - float alpha = 1.0f; + Rectangle watermarkRect = new Rectangle(-50, GameMain.GraphicsHeight - 80, 50 + (int)(Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X) * 1.2f), 100); + float alpha = 1.0f; - int yOffset = 0; + int yOffset = 0; - if (Screen.Selected == GameMain.GameScreen) - { - yOffset = (int)(-HUDLayoutSettings.ChatBoxArea.Height * 1.2f); - watermarkRect.Y += yOffset; - } + if (Screen.Selected == GameMain.GameScreen) + { + yOffset = (int)(-HUDLayoutSettings.ChatBoxArea.Height * 1.2f); + watermarkRect.Y += yOffset; + } - if (Screen.Selected == GameMain.GameScreen || Screen.Selected == GameMain.SubEditorScreen) - { - alpha = 0.2f; - } + if (Screen.Selected == GameMain.GameScreen || Screen.Selected == GameMain.SubEditorScreen) + { + alpha = 0.2f; + } - Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, watermarkRect, Color.Black * 0.8f * alpha); - LargeFont.DrawString(spriteBatch, line1, - new Vector2(10, GameMain.GraphicsHeight - 30 - LargeFont.MeasureString(line1).Y + yOffset), Color.White * 0.6f * alpha); - Font.DrawString(spriteBatch, line2, + GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( + spriteBatch, watermarkRect, Color.Black * 0.8f * alpha); + GUIStyle.LargeFont.DrawString(spriteBatch, line1, + new Vector2(10, GameMain.GraphicsHeight - 30 - GUIStyle.LargeFont.MeasureString(line1).Y + yOffset), Color.White * 0.6f * alpha); + GUIStyle.Font.DrawString(spriteBatch, line2, new Vector2(10, GameMain.GraphicsHeight - 30 + yOffset), Color.White * 0.6f * alpha); - if (Screen.Selected != GameMain.GameScreen) - { - var buttonRect = - new Rectangle(20 + (int)Math.Max(LargeFont.MeasureString(line1).X, Font.MeasureString(line2).X), GameMain.GraphicsHeight - (int)(45 * Scale) + yOffset, (int)(150 * Scale), (int)(40 * Scale)); - if (DrawButton(spriteBatch, buttonRect, "Report Bug", Style.GetComponentStyle("GUIBugButton").Color * 0.8f)) + if (Screen.Selected != GameMain.GameScreen) { - GameMain.Instance.ShowBugReporter(); + var buttonRect = + new Rectangle(20 + (int)Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X), GameMain.GraphicsHeight - (int)(45 * Scale) + yOffset, (int)(150 * Scale), (int)(40 * Scale)); + if (DrawButton(spriteBatch, buttonRect, "Report Bug", GUIStyle.GetComponentStyle("GUIBugButton").Color * 0.8f)) + { + GameMain.Instance.ShowBugReporter(); + } } - } #endif if (DisableHUD) @@ -388,12 +303,12 @@ namespace Barotrauma { DrawString(spriteBatch, new Vector2(10, 10), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) { DrawString(spriteBatch, new Vector2(10, 25), $"Physics: {GameMain.CurrentUpdateRate}", - (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, SmallFont); + (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } @@ -403,15 +318,15 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(300, y), "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", - Style.Green, Color.Black * 0.8f, font: SmallFont); - y += 15; - GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Style.Green); + GUIStyle.Green, Color.Black * 0.8f, font: GUIStyle.SmallFont); + y += 15; + GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: GUIStyle.Green); y += 50; DrawString(spriteBatch, new Vector2(300, y), "Update - Avg: " + GameMain.PerformanceCounter.UpdateTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.UpdateTimeGraph.LargestValue().ToString("0.00") + " ms", - Color.LightBlue, Color.Black * 0.8f, font: SmallFont); + Color.LightBlue, Color.Black * 0.8f, font: GUIStyle.SmallFont); y += 15; GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Color.LightBlue); y += 50; @@ -420,19 +335,25 @@ namespace Barotrauma float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key); DrawString(spriteBatch, new Vector2(300, y), key + ": " + elapsedMillisecs.ToString("0.00"), - Color.Lerp(Color.LightGreen, GUI.Style.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, SmallFont); + Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; } + if (Powered.Grids != null) + { + DrawString(spriteBatch, new Vector2(300, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += 15; + } + if (Settings.EnableDiagnostics) { - DrawString(spriteBatch, new Vector2(320, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 15), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 30), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 45), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 60), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 75), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(320, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(320, y + 15), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(320, y + 30), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(320, y + 45), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(320, y + 60), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(320, y + 75), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } @@ -440,56 +361,56 @@ namespace Barotrauma { DrawString(spriteBatch, new Vector2(10, 25), "Physics: " + GameMain.World.UpdateTime, - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); DrawString(spriteBatch, new Vector2(10, 40), $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)", - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (Screen.Selected.Cam != null) { DrawString(spriteBatch, new Vector2(10, 55), "Camera pos: " + Screen.Selected.Cam.Position.ToPoint() + ", zoom: " + Screen.Selected.Cam.Zoom, - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } if (Submarine.MainSub != null) { DrawString(spriteBatch, new Vector2(10, 70), "Sub pos: " + Submarine.MainSub.Position.ToPoint(), - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } DrawString(spriteBatch, new Vector2(10, 90), "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, - Color.Lerp(GUI.Style.Green, GUI.Style.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, SmallFont); + Color.Lerp(GUIStyle.Green, GUIStyle.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (loadedSpritesText == null || DateTime.Now > loadedSpritesUpdateTime) { loadedSpritesText = "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)"; loadedSpritesUpdateTime = DateTime.Now + new TimeSpan(0, 0, seconds: 5); } - DrawString(spriteBatch, new Vector2(10, 115), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(10, 115), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (debugDrawSounds) { int y = 0; DrawString(spriteBatch, new Vector2(500, y), - "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; DrawString(spriteBatch, new Vector2(500, y), - "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, SmallFont); + "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; DrawString(spriteBatch, new Vector2(500, y), - "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, SmallFont); + "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; DrawString(spriteBatch, new Vector2(500, y), - "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; for (int i = 0; i < SoundManager.SOURCE_COUNT; i++) @@ -538,27 +459,27 @@ namespace Barotrauma } } - DrawString(spriteBatch, new Vector2(500, y), soundStr, clr, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(500, y), soundStr, clr, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; } } else { DrawString(spriteBatch, new Vector2(500, 0), - "Ctrl+S to show sound debug info", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Ctrl+S to show sound debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } if (debugDrawEvents) { DrawString(spriteBatch, new Vector2(10, 300), - "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); GameMain.GameSession?.EventManager?.DebugDrawHUD(spriteBatch, 315); } else { DrawString(spriteBatch, new Vector2(10, 300), - "Ctrl+E to show EventManager debug info", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Ctrl+E to show EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) @@ -570,17 +491,17 @@ namespace Barotrauma $"Ctrl+2 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[1]) ? "hide" : "show")} faction reputations, \n" + $"Ctrl+3 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[2]) ? "hide" : "show")} upgrade levels, \n" + $"Ctrl+4 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[3]) ? "hide" : "show")} upgrade prices"; - var (x, y) = SmallFont.MeasureString(text); + var (x, y) = GUIStyle.SmallFont.MeasureString(text); Vector2 pos = new Vector2(GameMain.GraphicsWidth - (x + 10), 300); - DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); pos.Y += y + 8; campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, debugDrawMetadataOffset, ignoredMetadataInfo); } else { const string text = "Ctrl+M to show campaign metadata debug info"; - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (SmallFont.MeasureString(text).X + 10), 300), - text, Color.White, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (GUIStyle.SmallFont.MeasureString(text).X + 10), 300), + text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } @@ -616,9 +537,9 @@ namespace Barotrauma foreach (string str in strings) { - Vector2 stringSize = SmallFont.MeasureString(str); + Vector2 stringSize = GUIStyle.SmallFont.MeasureString(str); - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)stringSize.X - padding, yPos), str, Color.LightGreen, Color.Black, 0, SmallFont); + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)stringSize.X - padding, yPos), str, Color.LightGreen, Color.Black, 0, GUIStyle.SmallFont); yPos += (int)stringSize.Y + padding / 2; } } @@ -639,7 +560,7 @@ namespace Barotrauma DrawMessages(spriteBatch, cam); - if (MouseOn != null && !string.IsNullOrWhiteSpace(MouseOn.ToolTip)) + if (MouseOn != null && !MouseOn.ToolTip.IsNullOrWhiteSpace()) { MouseOn.DrawToolTip(spriteBatch); } @@ -651,7 +572,7 @@ namespace Barotrauma { case ItemPrefab itemPrefab: { - var sprite = itemPrefab.InventoryIcon ?? itemPrefab.sprite; + var sprite = itemPrefab.InventoryIcon ?? itemPrefab.Sprite; sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); break; } @@ -660,12 +581,13 @@ namespace Barotrauma var (x, y) = PlayerInput.MousePosition; foreach (var pair in iPrefab.DisplayEntities) { - Rectangle dRect = pair.Second; + Rectangle dRect = pair.Item2; dRect = new Rectangle(x: (int)(dRect.X * iPrefab.Scale + x), y: (int)(dRect.Y * iPrefab.Scale - y), width: (int)(dRect.Width * iPrefab.Scale), height: (int)(dRect.Height * iPrefab.Scale)); - pair.First.DrawPlacing(spriteBatch, dRect, pair.First.Scale * iPrefab.Scale); + MapEntityPrefab prefab = MapEntityPrefab.Find("", pair.Item1); + prefab.DrawPlacing(spriteBatch, dRect, prefab.Scale * iPrefab.Scale); } break; } @@ -678,14 +600,14 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable); - - if (GameMain.GameSession?.CrewManager is { DraggedOrder: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true }) + + if (GameMain.GameSession?.CrewManager is { DraggedOrderPrefab: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true }) { float spriteSize = Math.Max(orderSprite.size.X, orderSprite.size.Y); orderSprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, color, orderSprite.size / 2f, scale: 32f / spriteSize * Scale); } - var sprite = MouseCursorSprites[(int)MouseCursor] ?? MouseCursorSprites[(int)CursorState.Default]; + var sprite = MouseCursorSprites[MouseCursor] ?? MouseCursorSprites[CursorState.Default]; sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f); spriteBatch.End(); @@ -891,13 +813,13 @@ namespace Barotrauma GUIMessageBox.AddActiveToGUIUpdateList(); GUIContextMenu.AddActiveToGUIUpdateList(); - if (pauseMenuOpen) + if (PauseMenuOpen) { PauseMenu.AddToGUIUpdateList(); } - if (settingsMenuOpen) + if (SettingsMenuOpen) { - GameMain.Config.SettingsFrame.AddToGUIUpdateList(); + SettingsMenuContainer.AddToGUIUpdateList(); } //the "are you sure you want to quit" prompts are drawn on top of everything else @@ -1303,7 +1225,7 @@ namespace Barotrauma private static void UpdateSavingIndicator(float deltaTime) { - if (Style.SavingIndicator == null) { return; } + if (GUIStyle.SavingIndicator == null) { return; } lock (mutex) { if (timeUntilSavingIndicatorDisabled.HasValue) @@ -1349,7 +1271,7 @@ namespace Barotrauma } if (IsSavingIndicatorVisible) { - savingIndicatorSpriteIndex = (savingIndicatorSpriteIndex + 15.0f * deltaTime) % (Style.SavingIndicator.FrameCount + 1); + savingIndicatorSpriteIndex = (savingIndicatorSpriteIndex + 15.0f * deltaTime) % (GUIStyle.SavingIndicator.FrameCount + 1); } } } @@ -1439,7 +1361,7 @@ namespace Barotrauma public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1) { - DrawLine(sb, t, start, end, clr, depth, (int)width); + DrawLine(sb, solidWhiteTexture, start, end, clr, depth, (int)width); } public static void DrawLine(SpriteBatch sb, Sprite sprite, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1) @@ -1482,21 +1404,26 @@ namespace Barotrauma depth); } - public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null) + public static void DrawString(SpriteBatch sb, Vector2 pos, LocalizedString text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null) { - if (font == null) font = Font; + DrawString(sb, pos, text.Value, color, backgroundColor, backgroundPadding, font); + } + + public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit) + { + if (font == null) font = GUIStyle.Font; if (backgroundColor != null) { Vector2 textSize = font.MeasureString(text); DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true); } - font.DrawString(sb, text, pos, color); + font.DrawString(sb, text, pos, color, forceUpperCase: forceUpperCase); } - public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, List richTextData, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null, float depth = 0.0f) + public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, in ImmutableArray? richTextData, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, float depth = 0.0f) { - if (font == null) font = Font; + if (font == null) font = GUIStyle.Font; if (backgroundColor != null) { Vector2 textSize = font.MeasureString(text); @@ -1506,6 +1433,63 @@ namespace Barotrauma font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, richTextData); } + private const int DonutSegments = 30; + private static readonly ImmutableArray canonicalCircle + = Enumerable.Range(0, DonutSegments) + .Select(i => i * (2.0f * MathF.PI / DonutSegments)) + .Select(angle => new Vector2(MathF.Cos(angle), MathF.Sin(angle))) + .ToImmutableArray(); + private static readonly VertexPositionColorTexture[] donutVerts = new VertexPositionColorTexture[DonutSegments * 4]; + + public static void DrawDonutSection( + SpriteBatch sb, Vector2 center, Range radii, float sectionRad, Color clr, float depth = 0.0f) + { + float getRadius(int vertexIndex) + => (vertexIndex % 4) switch + { + 0 => radii.End, + 1 => radii.End, + 2 => radii.Start, + 3 => radii.Start, + _ => throw new InvalidOperationException() + }; + int getDirectionIndex(int vertexIndex) + => (vertexIndex % 4) switch + { + 0 => (vertexIndex / 4) + 0, + 1 => (vertexIndex / 4) + 1, + 2 => (vertexIndex / 4) + 0, + 3 => (vertexIndex / 4) + 1, + _ => throw new InvalidOperationException() + }; + + float sectionProportion = sectionRad / (MathF.PI * 2.0f); + int maxDirectionIndex = Math.Min(DonutSegments, (int)MathF.Ceiling(sectionProportion * DonutSegments)); + + Vector2 getDirection(int vertexIndex) + { + int directionIndex = getDirectionIndex(vertexIndex); + Vector2 dir = canonicalCircle[directionIndex % DonutSegments]; + if (maxDirectionIndex > 0 && directionIndex >= maxDirectionIndex) + { + float maxSectionProportion = (float)maxDirectionIndex / DonutSegments; + dir = Vector2.Lerp( + canonicalCircle[maxDirectionIndex - 1], + canonicalCircle[maxDirectionIndex % DonutSegments], + 1.0f - (maxSectionProportion - sectionProportion) * DonutSegments); + } + + return new Vector2(dir.Y, -dir.X); + } + + for (int vertexIndex = 0; vertexIndex < maxDirectionIndex * 4; vertexIndex++) + { + donutVerts[vertexIndex].Color = clr; + donutVerts[vertexIndex].Position = new Vector3(center + getDirection(vertexIndex) * getRadius(vertexIndex), 0.0f); + } + sb.Draw(solidWhiteTexture, donutVerts, depth, count: maxDirectionIndex); + } + public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1) { if (size.X < 0) @@ -1525,15 +1509,15 @@ namespace Barotrauma { if (isFilled) { - sb.Draw(t, rect, null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, rect, null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); } else { Rectangle srcRect = new Rectangle(0, 0, 1, 1); - sb.Draw(t, new Vector2(rect.X, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height), SpriteEffects.None, depth); - sb.Draw(t, new Vector2(rect.X + thickness, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); - sb.Draw(t, new Vector2(rect.X + thickness, rect.Bottom - thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); - sb.Draw(t, new Vector2(rect.Right - thickness, rect.Y + thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height - thickness * 2f), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.X, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Bottom - thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.Right - thickness, rect.Y + thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height - thickness * 2f), SpriteEffects.None, depth); } } @@ -1555,7 +1539,7 @@ namespace Barotrauma size.Y = -size.Y; } - sb.Draw(t, start, null, clr, 0f, Vector2.Zero, size, SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, start, null, clr, 0f, Vector2.Zero, size, SpriteEffects.None, depth); } public static void DrawRectangle(SpriteBatch sb, Vector2 center, float width, float height, float rotation, Color clr, float depth = 0.0f, float thickness = 1) @@ -1621,14 +1605,14 @@ namespace Barotrauma Vector2 origin; try { - origin = Font.MeasureString(text) / 2; + origin = GUIStyle.Font.MeasureString(text) / 2; } catch { origin = Vector2.Zero; } - Font.DrawString(sb, text, new Vector2(rect.Center.X, rect.Center.Y), Color.White, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f); + GUIStyle.Font.DrawString(sb, text, new Vector2(rect.Center.X, rect.Center.Y), Color.White, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f); return clicked; } @@ -1698,7 +1682,7 @@ namespace Barotrauma public static void DrawSineWithDots(SpriteBatch spriteBatch, Vector2 from, Vector2 dir, float amplitude, float length, float scale, int pointCount, Color color, int dotSize = 2) { Vector2 up = dir.Right(); - //DrawLine(spriteBatch, from, from + dir, GUI.Style.Red); + //DrawLine(spriteBatch, from, from + dir, GUIStyle.Red); //DrawLine(spriteBatch, from, from + up * dir.Length(), Color.Blue); for (int i = 0; i < pointCount; i++) { @@ -1715,8 +1699,8 @@ namespace Barotrauma private static void DrawSavingIndicator(SpriteBatch spriteBatch) { - if (!IsSavingIndicatorVisible || Style.SavingIndicator == null) { return; } - var sheet = Style.SavingIndicator; + if (!IsSavingIndicatorVisible || GUIStyle.SavingIndicator == null) { return; } + var sheet = GUIStyle.SavingIndicator; Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2(); sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale)); } @@ -1907,9 +1891,9 @@ namespace Barotrauma return CreateElements(count, parent, constructor, null, absoluteSize, anchor, pivot, null, null, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal); } - public static GUIComponent CreateEnumField(Enum value, int elementHeight, string name, RectTransform parent, string toolTip = null, ScalableFont font = null) + public static GUIComponent CreateEnumField(Enum value, int elementHeight, LocalizedString name, RectTransform parent, string toolTip = null, GUIFont font = null) { - font = font ?? SmallFont; + font = font ?? GUIStyle.SmallFont; var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementHeight), parent), color: Color.Transparent); new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform), name, font: font) { @@ -1928,10 +1912,10 @@ namespace Barotrauma return frame; } - public static GUIComponent CreateRectangleField(Rectangle value, int elementHeight, string name, RectTransform parent, string toolTip = null, ScalableFont font = null) + public static GUIComponent CreateRectangleField(Rectangle value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null) { var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent); - font = font ?? SmallFont; + font = font ?? GUIStyle.SmallFont; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), frame.RectTransform), name, font: font) { ToolTip = toolTip @@ -1944,7 +1928,7 @@ namespace Barotrauma 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), rectComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), RectComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { @@ -1973,10 +1957,10 @@ namespace Barotrauma return frame; } - public static GUIComponent CreatePointField(Point value, int elementHeight, string displayName, RectTransform parent, string toolTip = null) + public static GUIComponent CreatePointField(Point value, int elementHeight, LocalizedString displayName, RectTransform parent, LocalizedString toolTip = null) { var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent); - new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), displayName, font: SmallFont) + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -1988,11 +1972,11 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), vectorComponentLabels[i], font: SmallFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { - Font = SmallFont + Font = GUIStyle.SmallFont }; if (i == 0) @@ -2003,9 +1987,9 @@ namespace Barotrauma return frame; } - public static GUIComponent CreateVector2Field(Vector2 value, int elementHeight, string name, RectTransform parent, string toolTip = null, ScalableFont font = null, int decimalsToDisplay = 1) + public static GUIComponent CreateVector2Field(Vector2 value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null, int decimalsToDisplay = 1) { - font = font ?? SmallFont; + font = font ?? GUIStyle.SmallFont; var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent); new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), name, font: font) { @@ -2019,7 +2003,7 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), vectorComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { Font = font }; switch (i) { @@ -2035,7 +2019,7 @@ namespace Barotrauma return frame; } - public static void NotifyPrompt(string header, string body) + public static void NotifyPrompt(LocalizedString header, LocalizedString body) { GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); msgBox.Buttons[0].OnClicked = delegate @@ -2045,9 +2029,9 @@ namespace Barotrauma }; } - public static GUIMessageBox AskForConfirmation(string header, string body, Action onConfirm, Action onDeny = null) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null) { - string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); // Cancel button @@ -2068,9 +2052,9 @@ namespace Barotrauma return msgBox; } - public static GUIMessageBox PromptTextInput(string header, string body, Action onConfirm) + public static GUIMessageBox PromptTextInput(LocalizedString header, string body, Action onConfirm) { - string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, string.Empty, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); GUITextBox textBox = new GUITextBox(new RectTransform(Vector2.One, msgBox.Content.RectTransform), text: body) { @@ -2198,16 +2182,18 @@ namespace Barotrauma /// The elements will not be moved outside this area. If the parameter is not given, the elements are kept inside the window. public static void PreventElementOverlap(IList elements, IList disallowedAreas = null, Rectangle? clampArea = null) { + List sortedElements = elements.OrderByDescending(e => e.Rect.Width + e.Rect.Height).ToList(); + Rectangle area = clampArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); - for (int i = 0; i < elements.Count; i++) + for (int i = 0; i < sortedElements.Count; i++) { Point moveAmount = Point.Zero; - Rectangle rect1 = elements[i].Rect; + Rectangle rect1 = sortedElements[i].Rect; moveAmount.X += Math.Max(area.X - rect1.X, 0); moveAmount.X -= Math.Max(rect1.Right - area.Right, 0); moveAmount.Y += Math.Max(area.Y - rect1.Y, 0); moveAmount.Y -= Math.Max(rect1.Bottom - area.Bottom, 0); - elements[i].RectTransform.ScreenSpaceOffset += moveAmount; + sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount; } bool intersections = true; @@ -2215,18 +2201,18 @@ namespace Barotrauma while (intersections && iterations < 100) { intersections = false; - for (int i = 0; i < elements.Count; i++) + for (int i = 0; i < sortedElements.Count; i++) { - Rectangle rect1 = elements[i].Rect; - for (int j = i + 1; j < elements.Count; j++) + Rectangle rect1 = sortedElements[i].Rect; + for (int j = i + 1; j < sortedElements.Count; j++) { - Rectangle rect2 = elements[j].Rect; + Rectangle rect2 = sortedElements[j].Rect; if (!rect1.Intersects(rect2)) { continue; } intersections = true; Point centerDiff = rect1.Center - rect2.Center; //move the interfaces away from each other, in a random direction if they're at the same position - Vector2 moveAmount = centerDiff == Point.Zero ? Rand.Vector(1.0f) : Vector2.Normalize(centerDiff.ToVector2()); + Vector2 moveAmount = centerDiff == Point.Zero ? Vector2.UnitX + Rand.Vector(0.1f) : Vector2.Normalize(centerDiff.ToVector2()); //if the horizontal move amount is much larger than vertical, only move horizontally //(= attempt to place the elements side-by-side if they're more apart horizontally than vertically) @@ -2246,8 +2232,8 @@ namespace Barotrauma //move by 10 units in the desired direction and repeat until nothing overlaps //(or after 100 iterations, in which case we'll just give up and let them overlap) - elements[i].RectTransform.ScreenSpaceOffset += moveAmount1.ToPoint(); - elements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint(); + sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount1.ToPoint(); + sortedElements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint(); } if (disallowedAreas == null) { continue; } @@ -2265,7 +2251,7 @@ namespace Barotrauma //move by 10 units in the desired direction and repeat until nothing overlaps //(or after 100 iterations, in which case we'll just give up and let them overlap) - elements[i].RectTransform.ScreenSpaceOffset += (moveAmount1).ToPoint(); + sortedElements[i].RectTransform.ScreenSpaceOffset += (moveAmount1).ToPoint(); } } iterations++; @@ -2301,11 +2287,11 @@ namespace Barotrauma if (Screen.Selected == GameMain.MainMenuScreen) { return; } if (PreventPauseMenuToggle) { return; } - settingsMenuOpen = false; + SettingsMenuOpen = false; TogglePauseMenu(null, null); - if (pauseMenuOpen) + if (PauseMenuOpen) { Inventory.DraggingItems.Clear(); Inventory.DraggingInventory = null; @@ -2330,7 +2316,7 @@ namespace Barotrauma }; CreateButton("PauseMenuResume", buttonContainer, null); - CreateButton("PauseMenuSettings", buttonContainer, () => { settingsMenuOpen = !settingsMenuOpen; }); + CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true); bool IsOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedOutpost; if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null) @@ -2407,7 +2393,7 @@ namespace Barotrauma { if (string.IsNullOrEmpty(verificationTextTag)) { - pauseMenuOpen = false; + PauseMenuOpen = false; action?.Invoke(); } else @@ -2422,13 +2408,13 @@ namespace Barotrauma void CreateVerificationPrompt(string textTag, Action confirmAction) { var msgBox = new GUIMessageBox("", TextManager.Get(textTag), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; msgBox.Buttons[0].OnClicked = (_, __) => { - pauseMenuOpen = false; + PauseMenuOpen = false; confirmAction?.Invoke(); return true; }; @@ -2439,8 +2425,8 @@ namespace Barotrauma private static bool TogglePauseMenu(GUIButton button, object obj) { - pauseMenuOpen = !pauseMenuOpen; - if (!pauseMenuOpen && PauseMenu != null) + PauseMenuOpen = !PauseMenuOpen; + if (!PauseMenuOpen && PauseMenu != null) { PauseMenu.RectTransform.Parent = null; PauseMenu = null; @@ -2451,10 +2437,21 @@ namespace Barotrauma /// /// Displays a message at the center of the screen, automatically preventing overlapping with other centered messages. TODO: Allow to show messages at the middle of the screen (instead of the top center). /// - public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, ScalableFont font = null) + /// + public static void AddMessage(LocalizedString message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) + { + AddMessage(message.Value, color, lifeTime, playSound, font); + } + + public static void AddMessage(LocalizedString message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1) + { + AddMessage(message.Value, color, pos, velocity, lifeTime, playSound, soundType, subId); + } + + public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) { if (messages.Any(msg => msg.Text == message)) { return; } - messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? LargeFont)); + messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont)); if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); } } @@ -2462,7 +2459,7 @@ namespace Barotrauma { Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.ID == subId); - var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, Font, sub: sub); + var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub); if (playSound) { SoundPlayer.PlayUISound(soundType); } bool overlapFound = true; int tries = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 52b34ece5..1846bc809 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -116,32 +116,32 @@ namespace Barotrauma get { return Frame.FlashTimer; } } - public override ScalableFont Font + public override GUIFont Font { get { - return (textBlock == null) ? GUI.Font : textBlock.Font; + return (textBlock == null) ? GUIStyle.Font : textBlock.Font; } set { base.Font = value; - if (textBlock != null) textBlock.Font = value; + if (textBlock != null) { textBlock.Font = value; } } } - public string Text + public LocalizedString Text { get { return textBlock.Text; } set { textBlock.Text = value; } } - public bool ForceUpperCase + public ForceUpperCase ForceUpperCase { get { return textBlock.ForceUpperCase; } set { textBlock.ForceUpperCase = value; } } - public override string ToolTip + public override RichString ToolTip { get { @@ -160,39 +160,36 @@ namespace Barotrauma private bool flashed; public GUISoundType ClickSound { get; set; } = GUISoundType.Click; - - public GUIButton(RectTransform rectT, string text = "", Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) + + public GUIButton(RectTransform rectT, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : this(rectT, new RawLString(""), textAlignment, style, color) { } + + public GUIButton(RectTransform rectT, LocalizedString text, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) { CanBeFocused = true; HoverCursor = CursorState.Hand; frame = new GUIFrame(new RectTransform(Vector2.One, rectT), style) { CanBeFocused = false }; - if (style != null) { GUI.Style.Apply(frame, style == "" ? "GUIButton" : style); } + if (style != null) { GUIStyle.Apply(frame, style == "" ? "GUIButton" : style); } if (color.HasValue) { this.color = frame.Color = color.Value; } + + var selfStyle = Style; textBlock = new GUITextBlock(new RectTransform(Vector2.One, rectT, Anchor.Center), text, textAlignment: textAlignment, style: null) { - TextColor = this.style == null ? Color.Black : this.style.TextColor, - HoverTextColor = this.style == null ? Color.Black : this.style.HoverTextColor, - SelectedTextColor = this.style == null ? Color.Black : this.style.SelectedTextColor, + TextColor = selfStyle?.TextColor ?? Color.Black, + HoverTextColor = selfStyle?.HoverTextColor ?? Color.Black, + SelectedTextColor = selfStyle?.SelectedTextColor ?? Color.Black, CanBeFocused = false }; - if (rectT.Rect.Height == 0 && !string.IsNullOrEmpty(text)) + if (rectT.Rect.Height == 0 && !text.IsNullOrEmpty()) { RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(textBlock.Text).Y)); RectTransform.MinSize = textBlock.RectTransform.MinSize = new Point(0, System.Math.Max(rectT.MinSize.Y, Rect.Height)); TextBlock.SetTextPos(); } - GUI.Style.Apply(textBlock, "", this); - - //if the text is in chinese/korean/japanese and we're not using a CJK-compatible font, - //use the default CJK font as a fallback - if (TextManager.IsCJK(textBlock.Text) && !textBlock.Font.IsCJK) - { - textBlock.Font = GUI.CJKFont; - } + GUIStyle.Apply(textBlock, "", this); Enabled = true; } @@ -217,7 +214,7 @@ namespace Barotrauma float expand = (pulseExpand * 20.0f) * GUI.Scale; expandRect.Inflate(expand, expand); - GUI.Style.ButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); + GUIStyle.EndRoundButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index e9ce18130..e78e4e160 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -8,6 +8,7 @@ using System.Xml.Linq; using Barotrauma.IO; using RestSharp; using System.Net; +using System.Collections.Immutable; namespace Barotrauma { @@ -66,7 +67,7 @@ namespace Barotrauma { foreach (GUIComponent child in Children) { - if (child.UserData == obj || (child.userData != null && child.userData.Equals(obj))) { return child; } + if (child.UserData == obj || (child.UserData != null && child.UserData.Equals(obj))) { return child; } } return null; } @@ -107,7 +108,7 @@ namespace Barotrauma } public GUIComponent FindChild(object userData, bool recursive = false) { - var matchingChild = Children.FirstOrDefault(c => c.userData == userData); + var matchingChild = Children.FirstOrDefault(c => c.UserData == userData); if (recursive && matchingChild == null) { foreach (GUIComponent child in Children) @@ -122,7 +123,7 @@ namespace Barotrauma public IEnumerable FindChildren(object userData) { - return Children.Where(c => c.userData == userData); + return Children.Where(c => c.UserData == userData); } public IEnumerable FindChildren(Func predicate) @@ -161,9 +162,7 @@ namespace Barotrauma protected Alignment alignment; - protected GUIComponentStyle style; - - protected object userData; + protected Identifier[] styleHierarchy; public bool CanBeFocused; @@ -206,16 +205,14 @@ namespace Barotrauma } } - public virtual ScalableFont Font + public virtual GUIFont Font { get; set; } - - // Use the rawtooltip when copying displayed tooltips so that any possible color-data related values are translated over as well - public string RawToolTip; - private string toolTip; - public virtual string ToolTip + + private RichString toolTip; + public virtual RichString ToolTip { get { @@ -223,18 +220,12 @@ namespace Barotrauma } set { - RawToolTip = value; - TooltipRichTextData = RichTextData.GetRichTextData(value, out value); toolTip = value; } } - public List TooltipRichTextData = null; - public GUIComponentStyle Style - { - get { return style; } - } + => GUIComponentStyle.FromHierarchy(styleHierarchy); public bool Visible { @@ -258,8 +249,8 @@ namespace Barotrauma protected Rectangle ClampRect(Rectangle r) { - if (Parent == null || !ClampMouseRectToParent) { return r; } - Rectangle parentRect = Parent.ClampRect(Parent.Rect); + if (Parent is null) { return r; } + Rectangle parentRect = !Parent.ClampMouseRectToParent ? Parent.Rect : Parent.ClampRect(Parent.Rect); if (parentRect.Width <= 0 || parentRect.Height <= 0) { return Rectangle.Empty; } if (parentRect.X > r.X) { @@ -293,11 +284,13 @@ namespace Barotrauma } public bool ClampMouseRectToParent { get; set; } = false; + public virtual Rectangle MouseRect { get { if (!CanBeFocused) { return Rectangle.Empty; } + return ClampMouseRectToParent ? ClampRect(Rect) : Rect; } } @@ -310,13 +303,13 @@ namespace Barotrauma protected ComponentState _state; protected ComponentState _previousState; - protected bool selected; + protected bool isSelected; public virtual bool Selected { - get { return selected; } + get { return isSelected; } set { - selected = value; + isSelected = value; foreach (var child in Children) { child.Selected = value; @@ -338,11 +331,8 @@ namespace Barotrauma } } - public object UserData - { - get { return userData; } - set { userData = value; } - } + #warning TODO: this is cursed, stop using this + public object UserData; public int CountChildren { @@ -417,20 +407,20 @@ namespace Barotrauma Visible = true; OutlineColor = Color.Transparent; - Font = GUI.Font; + Font = GUIStyle.Font; CanBeFocused = true; - if (style != null) { GUI.Style.Apply(this, style); } + if (style != null) { GUIStyle.Apply(this, style); } } protected GUIComponent(string style) { Visible = true; OutlineColor = Color.Transparent; - Font = GUI.Font; + Font = GUIStyle.Font; CanBeFocused = true; - if (style != null) { GUI.Style.Apply(this, style); } + if (style != null) { GUIStyle.Apply(this, style); } } #region Updating @@ -486,7 +476,7 @@ namespace Barotrauma { if (GUI.IsMouseOn(this) && PlayerInput.SecondaryMouseButtonClicked()) { - OnSecondaryClicked?.Invoke(this, userData); + OnSecondaryClicked?.Invoke(this, UserData); } } @@ -704,7 +694,7 @@ namespace Barotrauma if (GlowOnSelect && State == ComponentState.Selected) { - GUI.UIGlow.Draw(spriteBatch, Rect, SelectedColor); + GUIStyle.UIGlow.Draw(spriteBatch, Rect, SelectedColor); } if (flashTimer > 0.0f) @@ -724,7 +714,7 @@ namespace Barotrauma } else { - var glow = useCircularFlash ? GUI.UIGlowCircular : GUI.UIGlow; + var glow = useCircularFlash ? GUIStyle.UIGlowCircular : GUIStyle.UIGlow; glow.Draw(spriteBatch, flashRect, flashColor * (float)Math.Sin(flashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f)); @@ -738,24 +728,24 @@ namespace Barotrauma public void DrawToolTip(SpriteBatch spriteBatch) { if (!Visible) { return; } - DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipRichTextData); + DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect); } - public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Vector2 pos, List richTextData = null) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) { - if (Tutorials.Tutorial.ContentRunning) { return; } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); Point padding = new Point((int)(10 * GUI.Scale)); - if (toolTipBlock == null || (string)toolTipBlock.userData != toolTip) + if (toolTipBlock == null || (RichString)toolTipBlock.UserData != toolTip) { - toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); + toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), toolTip, font: GUIStyle.SmallFont, wrap: true, style: "GUIToolTip"); toolTipBlock.RectTransform.NonScaledSize = new Point( - (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), - (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); - toolTipBlock.userData = toolTip; + (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), + (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); + toolTipBlock.UserData = toolTip; } toolTipBlock.RectTransform.AbsoluteOffset = pos.ToPoint(); @@ -764,21 +754,21 @@ namespace Barotrauma toolTipBlock.DrawManually(spriteBatch); } - public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle targetElement, List richTextData = null) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement) { - if (Tutorials.Tutorial.ContentRunning) { return; } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); Point padding = new Point((int)(10 * GUI.Scale)); - if (toolTipBlock == null || (string)toolTipBlock.userData != toolTip) + if (toolTipBlock == null || (RichString)toolTipBlock.UserData != toolTip) { - toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); + toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), toolTip, font: GUIStyle.SmallFont, wrap: true, style: "GUIToolTip"); toolTipBlock.RectTransform.NonScaledSize = new Point( (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); - toolTipBlock.userData = toolTip; + toolTipBlock.UserData = toolTip; } toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom); @@ -811,7 +801,7 @@ namespace Barotrauma this.useRectangleFlash = useRectangleFlash; this.useCircularFlash = useCircularFlash; this.flashDuration = flashDuration; - flashColor = (color == null) ? GUI.Style.Red : (Color)color; + flashColor = (color == null) ? GUIStyle.Red : (Color)color; } public void FadeOut(float duration, bool removeAfter, float wait = 0.0f) @@ -952,8 +942,7 @@ namespace Barotrauma ApplySizeRestrictions(style); } - - this.style = style; + styleHierarchy = GUIComponentStyle.ToHierarchy(style); } public void ApplySizeRestrictions(GUIComponentStyle style) @@ -972,11 +961,11 @@ namespace Barotrauma } } - public static GUIComponent FromXML(XElement element, RectTransform parent) + public static GUIComponent FromXML(ContentXElement element, RectTransform parent) { GUIComponent component = null; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase) && !CheckConditional(subElement)) { @@ -1027,7 +1016,7 @@ namespace Barotrauma if (component != null) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase)) { continue; } FromXML(subElement, component is GUIListBox listBox ? listBox.Content.RectTransform : component.RectTransform); @@ -1078,8 +1067,9 @@ namespace Barotrauma switch (attribute.Name.ToString().ToLowerInvariant()) { case "language": - string[] languages = element.GetAttributeStringArray(attribute.Name.ToString(), new string[0]); - if (!languages.Any(l => GameMain.Config.Language.Equals(l, StringComparison.OrdinalIgnoreCase))) { return false; } + var languages = element.GetAttributeIdentifierArray(attribute.Name.ToString(), Array.Empty()) + .Select(s => new LanguageIdentifier(s)); + if (!languages.Any(l => GameSettings.CurrentConfig.Language == l)) { return false; } break; case "gameversion": var version = new Version(attribute.Value); @@ -1136,23 +1126,12 @@ namespace Barotrauma if (element.Attribute("color") != null) { color = element.GetAttributeColor("color", Color.White); } float scale = element.GetAttributeFloat("scale", 1.0f); bool wrap = element.GetAttributeBool("wrap", true); - Alignment alignment = Alignment.Center; - Enum.TryParse(element.GetAttributeString("alignment", "Center"), out alignment); - ScalableFont font = GUI.Font; - switch (element.GetAttributeString("font", "Font").ToLowerInvariant()) + Alignment alignment = + element.GetAttributeEnum("alignment", text.Contains('\n') ? Alignment.Left : Alignment.Center); + GUIFont font; + if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out font)) { - case "font": - font = GUI.Font; - break; - case "smallfont": - font = GUI.SmallFont; - break; - case "largefont": - font = GUI.LargeFont; - break; - case "subheading": - font = GUI.SubHeadingFont; - break; + font = GUIStyle.Font; } var textBlock = new GUITextBlock(RectTransform.Load(element, parent), @@ -1265,7 +1244,7 @@ namespace Barotrauma }; } - private static GUIImage LoadGUIImage(XElement element, RectTransform parent) + private static GUIImage LoadGUIImage(ContentXElement element, RectTransform parent) { Sprite sprite; string url = element.GetAttributeString("url", ""); @@ -1298,11 +1277,11 @@ namespace Barotrauma return new GUIImage(RectTransform.Load(element, parent), sprite, scaleToFit: true); } - private static GUIButton LoadAccordion(XElement element, RectTransform parent) + private static GUIButton LoadAccordion(ContentXElement element, RectTransform parent) { var button = LoadGUIButton(element, parent); List content = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { var contentElement = FromXML(subElement, parent); if (contentElement != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index f752af7c2..5b4ae436a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -9,16 +9,23 @@ namespace Barotrauma { struct ContextMenuOption { - public string Label; + public LocalizedString Label; public Action OnSelected; public ContextMenuOption[]? SubOptions; public bool IsEnabled; - public string Tooltip; + public LocalizedString Tooltip; + + public ContextMenuOption(string labelTag, bool isEnabled, Action onSelected) + : this(TextManager.Get(labelTag), isEnabled, onSelected) { } + + public ContextMenuOption(Identifier labelTag, bool isEnabled, Action onSelected) + : this(TextManager.Get(labelTag), isEnabled, onSelected) { } + // Creates a regular context menu - public ContextMenuOption(string label, bool isEnabled, Action onSelected) + public ContextMenuOption(LocalizedString label, bool isEnabled, Action onSelected) { - Label = TextManager.Get(label, returnNull: true) ?? label; + Label = label; OnSelected = onSelected; IsEnabled = isEnabled; SubOptions = null; @@ -49,14 +56,14 @@ namespace Barotrauma /// Header text /// Background style /// list of context menu options - public GUIContextMenu(Vector2? position, string header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas)) + public GUIContextMenu(Vector2? position, LocalizedString header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas)) { Vector2 pos = position ?? PlayerInput.MousePosition; - ScalableFont headerFont = GUI.SubHeadingFont; - ScalableFont font = GUI.SmallFont; // font the context menu options use + GUIFont headerFont = GUIStyle.SubHeadingFont; + GUIFont font = GUIStyle.SmallFont; // font the context menu options use Vector4 padding = new Vector4(4), headerPadding = new Vector4(8); int horizontalPadding = (int) (padding.X + padding.Z), verticalPadding = (int) (padding.Y + padding.W); - bool hasHeader = !string.IsNullOrWhiteSpace(header); + bool hasHeader = !header.IsNullOrWhiteSpace(); //---------------------------------------------------------------------------------- // Estimate the size of the context menu @@ -111,7 +118,7 @@ namespace Barotrauma }; Options.Add(option, optionElement); - if (!string.IsNullOrWhiteSpace(option.Tooltip) && optionElement.Enabled) + if (!option.Tooltip.IsNullOrWhiteSpace() && optionElement.Enabled) { optionElement.ToolTip = option.Tooltip; } @@ -179,7 +186,7 @@ namespace Barotrauma public static GUIContextMenu CreateContextMenu(params ContextMenuOption[] options) => CreateContextMenu(PlayerInput.MousePosition, string.Empty, null, options); - public static GUIContextMenu CreateContextMenu(Vector2? pos, string header, Color? headerColor, params ContextMenuOption[] options) + public static GUIContextMenu CreateContextMenu(Vector2? pos, LocalizedString header, Color? headerColor, params ContextMenuOption[] options) { GUIContextMenu menu = new GUIContextMenu(pos,header, "GUIToolTip", options); if (headerColor != null) @@ -209,7 +216,7 @@ namespace Barotrauma /// String whose size to inflate by /// What font to use /// The size of the text - private Vector2 InflateSize(ref Point size, string label, ScalableFont font) + private Vector2 InflateSize(ref Point size, LocalizedString label, ScalableFont font) { Vector2 textSize = font.MeasureString(label); size.X = Math.Max((int) Math.Ceiling(textSize.X), size.X); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 6c2780430..b4dc7513d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -55,9 +55,8 @@ namespace Barotrauma { get { return listBox.SelectedComponent; } } - - // TODO: fix implicit hiding - public bool Selected + + public override bool Selected { get { @@ -97,7 +96,7 @@ namespace Barotrauma set { button.TextColor = value; } } - public override ScalableFont Font + public override GUIFont Font { get { return button?.Font ?? base.Font; } set @@ -142,13 +141,13 @@ namespace Barotrauma get { return selectedIndexMultiple; } } - public string Text + public LocalizedString Text { get { return button.Text; } set { button.Text = value; } } - public override string ToolTip + public override RichString ToolTip { get { @@ -162,8 +161,10 @@ namespace Barotrauma } } - public GUIDropDown(RectTransform rectT, string text = "", int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false) : base(style, rectT) + public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false) : base(style, rectT) { + text ??= new RawLString(""); + HoverCursor = CursorState.Hand; CanBeFocused = true; @@ -173,7 +174,7 @@ namespace Barotrauma { OnClicked = OnClicked }; - GUI.Style.Apply(button, "", this); + GUIStyle.Apply(button, "", this); button.TextBlock.SetTextPos(); Anchor listAnchor = dropAbove ? Anchor.TopCenter : Anchor.BottomCenter; @@ -184,13 +185,13 @@ namespace Barotrauma Enabled = !selectMultiple }; if (!selectMultiple) { listBox.OnSelected = SelectItem; } - GUI.Style.Apply(listBox, "GUIListBox", this); - GUI.Style.Apply(listBox.ContentBackground, "GUIListBox", this); + GUIStyle.Apply(listBox, "GUIListBox", this); + GUIStyle.Apply(listBox.ContentBackground, "GUIListBox", this); - if (button.Style.ChildStyles.ContainsKey("dropdownicon")) + if (button.Style.ChildStyles.ContainsKey("dropdownicon".ToIdentifier())) { icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), button.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, null, scaleToFit: true); - icon.ApplyStyle(button.Style.ChildStyles["dropdownicon"]); + icon.ApplyStyle(button.Style.ChildStyles["dropdownicon".ToIdentifier()]); } currentHighestParent = FindHighestParent(); @@ -244,8 +245,9 @@ namespace Barotrauma return parentHierarchy.Last(); } - public void AddItem(string text, object userData = null, string toolTip = "") + public void AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null) { + toolTip ??= ""; if (selectMultiple) { var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) @@ -261,7 +263,7 @@ namespace Barotrauma ToolTip = toolTip, OnSelected = (GUITickBox tb) => { - List texts = new List(); + List texts = new List(); selectedDataMultiple.Clear(); selectedIndexMultiple.Clear(); int i = 0; @@ -276,7 +278,7 @@ namespace Barotrauma } i++; } - button.Text = string.Join(", ", texts); + button.Text = LocalizedString.Join(", ", texts); // TODO: The callback is called at least twice, remove this? OnSelected?.Invoke(tb.Parent, tb.Parent.UserData); return true; @@ -368,8 +370,9 @@ namespace Barotrauma Dropped = !Dropped; if (Dropped && Enabled) { - OnDropped?.Invoke(this, userData); + OnDropped?.Invoke(this, UserData); listBox.UpdateScrollBarSize(); + listBox.UpdateDimensions(); GUI.KeyboardDispatcher.Subscriber = this; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index e7be6a3ec..23b891e34 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -177,6 +177,7 @@ namespace Barotrauma spriteBatch.Begin(blendState: BlendState, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } + var style = Style; if (style != null) { foreach (UISprite uiSprite in style.Sprites[State]) @@ -193,7 +194,7 @@ namespace Barotrauma } } } - else if (sprite?.Texture != null) + else if (sprite?.Texture is { IsDisposed: false }) { spriteBatch.Draw(sprite.Texture, Rect.Center.ToVector2(), sourceRect, currentColor * (currentColor.A / 255.0f), Rotation, origin, Scale, SpriteEffects, 0.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index d6e4efb6e..d21951cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -92,29 +92,19 @@ namespace Barotrauma foreach (RectTransform child in RectTransform.Children) { if (child.GUIComponent.IgnoreLayoutGroups) { continue; } - if (child.ScaleBasis == ScaleBasis.BothHeight) { child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); } - if (child.ScaleBasis == ScaleBasis.BothWidth) { child.MinSize = new Point(child.MinSize.X, child.Rect.Width); } - if (child.ScaleBasis == ScaleBasis.Smallest) + + switch (child.ScaleBasis) { - if (Rect.Width < Rect.Height) - { - child.MinSize = new Point(child.MinSize.X, child.Rect.Width); - } - else - { - child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); - } - } - if (child.ScaleBasis == ScaleBasis.Largest) - { - if (Rect.Width > Rect.Height) - { - child.MinSize = new Point(child.MinSize.X, child.Rect.Width); - } - else - { - child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); - } + case ScaleBasis.BothHeight: + case ScaleBasis.Smallest when Rect.Height <= Rect.Width: + case ScaleBasis.Largest when Rect.Height > Rect.Width: + child.MinSize = new Point((int)((child.Rect.Height * child.RelativeSize.X) / child.RelativeSize.Y), child.MinSize.Y); + break; + case ScaleBasis.BothWidth: + case ScaleBasis.Smallest when Rect.Width <= Rect.Height: + case ScaleBasis.Largest when Rect.Width > Rect.Height: + child.MinSize = new Point(child.MinSize.X, (int)((child.Rect.Width * child.RelativeSize.Y) / child.RelativeSize.X)); + break; } } @@ -144,42 +134,65 @@ namespace Barotrauma foreach (var child in RectTransform.Children) { if (child.GUIComponent.IgnoreLayoutGroups) { continue; } + + float currentStretchFactor = child.ScaleBasis == ScaleBasis.Normal ? stretchFactor : 1.0f; child.SetPosition(childAnchor); + + void advancePositionsAndCalculateChildSizes( + ref int childNonScaledSize, + ref float childRelativeSize, + int childMinSize, + int childMaxSize, + int childRectSize, + int selfRectSize) + { + if (child.IsFixedSize) + { + absPos += childNonScaledSize + absoluteSpacing; + } + else + { + absPos += (int)Math.Round(MathHelper.Clamp(childRectSize * currentStretchFactor, childMinSize, childMaxSize) + (absoluteSpacing * currentStretchFactor)); + if (stretch) + { + float relativeSize = + MathF.Round(childRelativeSize * currentStretchFactor * selfRectSize) / selfRectSize; + childRelativeSize = relativeSize; + } + } + } + + Point childNonScaledSize = child.NonScaledSize; + Vector2 childRelativeSize = child.RelativeSize; if (isHorizontal) { child.RelativeOffset = new Vector2(relPos, child.RelativeOffset.Y); child.AbsoluteOffset = new Point(absPos, child.AbsoluteOffset.Y); - if (child.IsFixedSize) - { - absPos += child.NonScaledSize.X + absoluteSpacing; - } - else - { - absPos += (int)(MathHelper.Clamp(child.Rect.Width * stretchFactor, child.MinSize.X, child.MaxSize.X) + (absoluteSpacing * stretchFactor)); - if (stretch) - { - child.RelativeSize = new Vector2(child.RelativeSize.X * stretchFactor, child.RelativeSize.Y); - } - } + advancePositionsAndCalculateChildSizes( + ref childNonScaledSize.X, + ref childRelativeSize.X, + child.MinSize.X, + child.MaxSize.X, + child.Rect.Width, + Rect.Width); } else { child.RelativeOffset = new Vector2(child.RelativeOffset.X, relPos); child.AbsoluteOffset = new Point(child.AbsoluteOffset.X, absPos); - if (child.IsFixedSize) - { - absPos += child.NonScaledSize.Y + absoluteSpacing; - } - else - { - absPos += (int)(MathHelper.Clamp(child.Rect.Height * stretchFactor, child.MinSize.Y, child.MaxSize.Y) + (absoluteSpacing * stretchFactor)); - if (stretch) - { - child.RelativeSize = new Vector2(child.RelativeSize.X, child.RelativeSize.Y * stretchFactor); - } - } + advancePositionsAndCalculateChildSizes( + ref childNonScaledSize.Y, + ref childRelativeSize.Y, + child.MinSize.Y, + child.MaxSize.Y, + child.Rect.Height, + Rect.Height); } + child.NonScaledSize = childNonScaledSize; + child.RelativeSize = childRelativeSize; relPos += relativeSpacing * stretchFactor; + if (isHorizontal) { relPos = MathF.Round(relPos * Rect.Width) / Rect.Width; } + else { relPos = MathF.Round(relPos * Rect.Height) / Rect.Height; } } needsToRecalculate = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 92b4362c2..a40d29880 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -148,7 +148,11 @@ namespace Barotrauma } // TODO: fix implicit hiding - public bool Selected { get; set; } + public override bool Selected + { + get { return isSelected; } + set { isSelected = value; } + } public IReadOnlyList AllSelected => selected; @@ -328,7 +332,7 @@ namespace Barotrauma }; if (style != null) { - GUI.Style.Apply(ContentBackground, "", this); + GUIStyle.Apply(ContentBackground, "", this); } if (color.HasValue) { @@ -435,7 +439,7 @@ namespace Barotrauma Vector2 topOffset = CalculateTopOffset(); int x = (int)topOffset.X; int y = (int)topOffset.Y; - + for (int i = 0; i < Content.CountChildren; i++) { GUIComponent child = Content.GetChild(i); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 4ec5929f3..72084edbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -10,7 +10,8 @@ namespace Barotrauma { public class GUIMessageBox : GUIFrame { - public static List MessageBoxes = new List(); + #warning TODO: change this to List and fix incorrect uses of this list + public readonly static List MessageBoxes = new List(); private static int DefaultWidth { get { return Math.Max(400, (int)(400 * (GameMain.GraphicsWidth / GUI.ReferenceResolution.X))); } @@ -70,14 +71,14 @@ namespace Barotrauma public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); - public GUIMessageBox(string headerText, string text, Vector2? relativeSize = null, Point? minSize = null) - : this(headerText, text, new string[] { "OK" }, relativeSize, minSize) + public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null) + : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize) { this.Buttons[0].OnClicked = Close; } - public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null, bool parseRichText = false) - : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") + public GUIMessageBox(RichString headerText, RichString text, LocalizedString[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null) + : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUIStyle.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { int width = (int)(DefaultWidth * type switch { @@ -125,23 +126,24 @@ namespace Barotrauma InnerFrame.RectTransform.ScreenSpaceOffset = new Point(-offset, offset); CanBeFocused = false; } - GUI.Style.Apply(InnerFrame, "", this); + GUIStyle.Apply(InnerFrame, "", this); this.type = type; Tag = tag; + #warning TODO: These should be broken into separate methods at least if (type == Type.Default || type == Type.Vote) { Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), - headerText, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Header, "", this); + headerText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - if (!string.IsNullOrWhiteSpace(text)) + if (!text.IsNullOrWhiteSpace()) { - Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Text, "", this); + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + GUIStyle.Apply(Text, "", this); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = new Point(Text.Rect.Width, Text.Rect.Height); Text.RectTransform.IsFixedSize = true; @@ -154,7 +156,7 @@ namespace Barotrauma }; int buttonSize = 35; - var buttonStyle = GUI.Style.GetComponentStyle("GUIButton"); + var buttonStyle = GUIStyle.GetComponentStyle("GUIButton"); if (buttonStyle != null && buttonStyle.Height.HasValue) { buttonSize = buttonStyle.Height.Value; @@ -189,7 +191,7 @@ namespace Barotrauma InnerFrame.RectTransform.AbsoluteOffset = new Point(0, GameMain.GraphicsHeight); CanBeFocused = false; AutoClose = true; - GUI.Style.Apply(InnerFrame, "", this); + GUIStyle.Apply(InnerFrame, "", this); var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), InnerFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) @@ -219,11 +221,11 @@ namespace Barotrauma }; InputType? closeInput = null; - if (GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None) { closeInput = InputType.Use; } - else if (GameMain.Config.KeyBind(InputType.Select).MouseButton == MouseButton.None) + else if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select].MouseButton == MouseButton.None) { closeInput = InputType.Select; } @@ -236,24 +238,24 @@ namespace Barotrauma { GUIButton btn = component as GUIButton; btn?.OnClicked(btn, btn.UserData); - btn?.Flash(GUI.Style.Green); + btn?.Flash(GUIStyle.Green); } }; } - Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Header, "", this); + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - if (!string.IsNullOrWhiteSpace(text)) + if (!text.IsNullOrWhiteSpace()) { - Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Text, "", this); + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + GUIStyle.Apply(Text, "", this); Content.Recalculate(); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = new Point(Text.Rect.Width, Text.Rect.Height); Text.RectTransform.IsFixedSize = true; - if (string.IsNullOrWhiteSpace(headerText)) + if (headerText.IsNullOrWhiteSpace()) { Content.ChildAnchor = Anchor.Center; } @@ -275,7 +277,7 @@ namespace Barotrauma else if (type == Type.Hint) { CanBeFocused = false; - GUI.Style.Apply(InnerFrame, "", this); + GUIStyle.Apply(InnerFrame, "", this); Point absoluteSpacing = GUIStyle.ItemFrameMargin.Multiply(1.0f / 5.0f); var verticalLayoutGroup = new GUILayoutGroup(new RectTransform(GetVerticalLayoutGroupSize(), parent: InnerFrame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) @@ -353,18 +355,18 @@ namespace Barotrauma }; Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); - GUI.Style.Apply(Header, "", this); + GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - if (!string.IsNullOrWhiteSpace(text)) + if (!text.IsNullOrWhiteSpace()) { Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); - GUI.Style.Apply(Text, "", this); + GUIStyle.Apply(Text, "", this); Content.Recalculate(); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = new Point(Text.Rect.Width, Text.Rect.Height); Text.RectTransform.IsFixedSize = true; - if (string.IsNullOrWhiteSpace(headerText)) + if (headerText.IsNullOrWhiteSpace()) { Header.RectTransform.Parent = null; Content.ChildAnchor = Anchor.Center; @@ -410,7 +412,7 @@ namespace Barotrauma /// /// Use to create a message box of Hint type /// - public GUIMessageBox(string hintIdentifier, string text, Sprite icon) : this("", text, new string[0], textAlignment: Alignment.CenterLeft, type: Type.Hint, icon: icon) + public GUIMessageBox(Identifier hintIdentifier, LocalizedString text, Sprite icon) : this("", text, Array.Empty(), textAlignment: Alignment.CenterLeft, type: Type.Hint, icon: icon) { if (InnerFrame.FindChild("dontshowagain", recursive: true) is GUITickBox dontShowAgainTickBox) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index e512e979a..a410c8db5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -162,7 +162,7 @@ namespace Barotrauma } } - public override ScalableFont Font + public override GUIFont Font { get { @@ -225,7 +225,7 @@ namespace Barotrauma var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); - GUI.Style.Apply(PlusButton, "PlusButton", this); + GUIStyle.Apply(PlusButton, "PlusButton", this); PlusButton.OnButtonDown += () => { pressedTimer = pressedDelay; @@ -246,7 +246,7 @@ namespace Barotrauma }; MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); - GUI.Style.Apply(MinusButton, "MinusButton", this); + GUIStyle.Apply(MinusButton, "MinusButton", this); MinusButton.OnButtonDown += () => { pressedTimer = pressedDelay; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs new file mode 100644 index 000000000..ccef64092 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -0,0 +1,395 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + public abstract class GUIPrefab : Prefab + { + public GUIPrefab(ContentXElement element, UIStyleFile file) : base(file, element) { } + + protected override Identifier DetermineIdentifier(XElement element) + { + return element.NameAsIdentifier(); + } + } + + public abstract class GUISelector where T : GUIPrefab + { + public readonly PrefabSelector Prefabs = new PrefabSelector(); + public readonly Identifier Identifier; + + public GUISelector(string identifier) + { + Identifier = identifier.ToIdentifier(); + } + } + + public class GUIFontPrefab : GUIPrefab + { + private readonly ContentXElement element; + private ScalableFont font; + public ScalableFont Font + { + get + { + if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } + return font; + } + } + + private ScalableFont cjkFont; + + public ScalableFont CjkFont + { + get + { + if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } + if (font.IsCJK) { return font; } + return cjkFont; + } + } + + public LanguageIdentifier Language { get; private set; } + + public GUIFontPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + this.element = element; + LoadFont(); + } + + private void LoadFont() + { + string fontPath = GetFontFilePath(element); + uint size = GetFontSize(element); + bool dynamicLoading = GetFontDynamicLoading(element); + bool isCJK = GetIsCJK(element); + font?.Dispose(); + cjkFont?.Dispose(); + font = new ScalableFont(fontPath, size, GameMain.Instance.GraphicsDevice, dynamicLoading, isCJK) + { + ForceUpperCase = element.GetAttributeBool("forceuppercase", false) + }; + if (!isCJK) + { + cjkFont = ExtractCjkFont(element) + ?? new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", + font.Size, GameMain.Instance.GraphicsDevice, dynamicLoading: true, isCJK: true); + cjkFont.ForceUpperCase = font.ForceUpperCase; + } + Language = GameSettings.CurrentConfig.Language; + } + + public override void Dispose() + { + font?.Dispose(); font = null; + cjkFont?.Dispose(); cjkFont = null; + } + + private ScalableFont ExtractCjkFont(ContentXElement element) + { + foreach (var subElement in element.Elements().Reverse()) + { + if (subElement.NameAsIdentifier() != "override") { continue; } + + if (subElement.GetAttributeBool("iscjk", false)) + { + return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); + } + } + return null; + } + + private string GetFontFilePath(ContentXElement element) + { + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + { + return subElement.GetAttributeContentPath("file")?.Value; + } + } + return element.GetAttributeContentPath("file")?.Value; + } + + private uint GetFontSize(XElement element, uint defaultSize = 14) + { + //check if any of the language override fonts want to override the font size as well + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + { + uint overrideFontSize = GetFontSize(subElement, 0); + if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.CurrentConfig.Graphics.TextScale); } + } + } + + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } + Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); + if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) + { + return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.CurrentConfig.Graphics.TextScale); + } + } + return (uint)Math.Round(defaultSize * GameSettings.CurrentConfig.Graphics.TextScale); + } + + private bool GetFontDynamicLoading(XElement element) + { + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + { + return subElement.GetAttributeBool("dynamicloading", false); + } + } + return element.GetAttributeBool("dynamicloading", false); + } + + private bool GetIsCJK(XElement element) + { + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + { + return subElement.GetAttributeBool("iscjk", false); + } + } + return element.GetAttributeBool("iscjk", false); + } + } + + public class GUIFont : GUISelector + { + public GUIFont(string identifier) : base(identifier) { } + + public bool HasValue => Prefabs.Any(); + + public ScalableFont Value => Prefabs.ActivePrefab.Font; + + public static implicit operator ScalableFont(GUIFont reference) => reference.Value; + + public bool ForceUpperCase => HasValue && Value.ForceUpperCase; + + public uint Size => HasValue ? Value.Size : 0; + + private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); + + private ScalableFont GetFontForStr(string str) => + TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font; + + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + { + DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth); + } + + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + { + GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth); + } + + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft) + { + DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth, alignment); + } + + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) + { + GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, alignment, forceUpperCase); + } + + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) + { + DrawString(sb, text.Value, position, color, forceUpperCase, italics); + } + + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) + { + GetFontForStr(text).DrawString(sb, text, position, color, forceUpperCase, italics); + } + + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) + { + GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); + } + + public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false) + { + return GetFontForStr(str).MeasureString(str, removeExtraSpacing); + } + + public Vector2 MeasureChar(char c) + { + return GetFontForStr($"{c}").MeasureChar(c); + } + + public string WrapText(string text, float width) + => GetFontForStr(text).WrapText(text, width); + + public string WrapText(string text, float width, int requestCharPos, out Vector2 requestedCharPos) + => GetFontForStr(text).WrapText(text, width, requestCharPos, out requestedCharPos); + + public string WrapText(string text, float width, out Vector2[] allCharPositions) + => GetFontForStr(text).WrapText(text, width, out allCharPositions); + + public float LineHeight => Value.LineHeight; + } + + public class GUIColorPrefab : GUIPrefab + { + public readonly Color Color; + + public GUIColorPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + Color = element.GetAttributeColor("color", Color.White); + } + + public override void Dispose() { } + } + + public class GUIColor : GUISelector + { + public GUIColor(string identifier) : base(identifier) { } + + public Color Value + { + get + { + return Prefabs.ActivePrefab.Color; + } + } + + public static implicit operator Color(GUIColor reference) => reference.Value; + + public static Color operator*(GUIColor value, float scale) + { + return value.Value * scale; + } + } + + public class GUISpritePrefab : GUIPrefab + { + public readonly UISprite Sprite; + + public GUISpritePrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + Sprite = new UISprite(element); + } + + public override void Dispose() + { + Sprite.Sprite.Remove(); + } + } + + public class GUISprite : GUISelector + { + public GUISprite(string identifier) : base(identifier) { } + + public UISprite Value + { + get + { + return Prefabs.ActivePrefab.Sprite; + } + } + + public static implicit operator UISprite(GUISprite reference) => reference.Value; + + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) + { + Value.Draw(spriteBatch, rect, color, spriteEffects); + } + } + + public class GUISpriteSheetPrefab : GUIPrefab + { + public readonly SpriteSheet SpriteSheet; + + public GUISpriteSheetPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + SpriteSheet = new SpriteSheet(element); + } + + public override void Dispose() + { + SpriteSheet.Remove(); + } + } + + public class GUISpriteSheet : GUISelector + { + public GUISpriteSheet(string identifier) : base(identifier) { } + + public SpriteSheet Value + { + get + { + return Prefabs.ActivePrefab.SpriteSheet; + } + } + + public int FrameCount => Value.FrameCount; + public Point FrameSize => Value.FrameSize; + + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0, float scale = 1, SpriteEffects spriteEffects = SpriteEffects.None) + { + Value.Draw(spriteBatch, pos, rotate, scale, spriteEffects); + } + + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate = 0, float scale = 1, SpriteEffects spriteEffects = SpriteEffects.None, float? depth = null) + { + Value.Draw(spriteBatch, pos, color, origin, rotate, scale, spriteEffects, depth); + } + + public void Draw(ISpriteBatch spriteBatch, int spriteIndex, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffects = SpriteEffects.None, float? depth = null) + { + Value.Draw(spriteBatch, spriteIndex, pos, color, origin, rotate, scale, spriteEffects, depth); + } + + public static implicit operator SpriteSheet(GUISpriteSheet reference) => reference.Value; + } + + public class GUICursorPrefab : GUIPrefab + { + public readonly Sprite[] Sprites; + + public GUICursorPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + Sprites = new Sprite[Enum.GetValues(typeof(CursorState)).Length]; + foreach (var subElement in element.Elements()) + { + CursorState state = subElement.GetAttributeEnum("state", CursorState.Default); + Sprites[(int)state] = new Sprite(subElement); + } + } + + public override void Dispose() + { + foreach (var sprite in Sprites) + { + sprite?.Remove(); + } + } + } + + public class GUICursor : GUISelector + { + public GUICursor(string identifier) : base(identifier) { } + + public Sprite this[CursorState k] => Prefabs.ActivePrefab.Sprites[(int)k]; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs index 0fe0f0675..02d1c9c29 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs @@ -47,9 +47,9 @@ namespace Barotrauma } isHorizontal = (Rect.Width > Rect.Height); frame = new GUIFrame(new RectTransform(Vector2.One, rectT)); - GUI.Style.Apply(frame, "", this); + GUIStyle.Apply(frame, "", this); slider = new GUIFrame(new RectTransform(Vector2.One, rectT)); - GUI.Style.Apply(slider, "Slider", this); + GUIStyle.Apply(slider, "Slider", this); this.showFrame = showFrame; this.barSize = barSize; Enabled = true; @@ -62,10 +62,10 @@ namespace Barotrauma public Rectangle GetSliderRect(float fillAmount) { Rectangle sliderArea = new Rectangle( - frame.Rect.X + (int)style.Padding.X, - frame.Rect.Y + (int)style.Padding.Y, - (int)(frame.Rect.Width - style.Padding.X - style.Padding.Z), - (int)(frame.Rect.Height - style.Padding.Y - style.Padding.W)); + frame.Rect.X + (int)Style.Padding.X, + frame.Rect.Y + (int)Style.Padding.Y, + (int)(frame.Rect.Width - Style.Padding.X - Style.Padding.Z), + (int)(frame.Rect.Height - Style.Padding.Y - Style.Padding.W)); Vector4 sliceBorderSizes = Vector4.Zero; if (slider.sprites.ContainsKey(slider.State) && (slider.sprites[slider.State].First()?.Slice ?? false)) @@ -116,10 +116,10 @@ namespace Barotrauma var sliderRect = GetSliderRect(barSize); - slider.RectTransform.AbsoluteOffset = new Point((int)style.Padding.X, (int)style.Padding.Y); + slider.RectTransform.AbsoluteOffset = new Point((int)Style.Padding.X, (int)Style.Padding.Y); slider.RectTransform.MaxSize = new Point( - (int)(Rect.Width - style.Padding.X + style.Padding.Z), - (int)(Rect.Height - style.Padding.Y + style.Padding.W)); + (int)(Rect.Width - Style.Padding.X + Style.Padding.Z), + (int)(Rect.Height - Style.Padding.Y + Style.Padding.W)); frame.Visible = showFrame; slider.Visible = BarSize > 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index aa44f2ffc..ff245d132 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; namespace Barotrauma @@ -29,7 +28,7 @@ namespace Barotrauma public bool IsBooleanSwitch; - public override string ToolTip + public override RichString ToolTip { get { return base.ToolTip; } set @@ -203,7 +202,7 @@ namespace Barotrauma CanBeFocused = true; this.isHorizontal = isHorizontal ?? (Rect.Width > Rect.Height); Frame = new GUIFrame(new RectTransform(Vector2.One, rectT)); - GUI.Style.Apply(Frame, IsHorizontal ? "GUIFrameHorizontal" : "GUIFrameVertical", this); + GUIStyle.Apply(Frame, IsHorizontal ? "GUIFrameHorizontal" : "GUIFrameVertical", this); this.barSize = barSize; Bar = new GUIButton(new RectTransform(Vector2.One, rectT, IsHorizontal ? Anchor.CenterLeft : Anchor.TopCenter), color: color, style: null); @@ -224,7 +223,7 @@ namespace Barotrauma break; } - GUI.Style.Apply(Bar, IsHorizontal ? "GUIButtonHorizontal" : "GUIButtonVertical", this); + GUIStyle.Apply(Bar, IsHorizontal ? "GUIButtonHorizontal" : "GUIButtonVertical", this); Bar.OnPressed = SelectBar; enabled = true; UpdateRect(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index f8cbd5414..0c8828ae5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -1,523 +1,195 @@ -using Barotrauma.Extensions; +using System; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System; -using System.Collections.Generic; -using System.Xml.Linq; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; namespace Barotrauma { - public class GUIStyle + public static class GUIStyle { - private Dictionary componentStyles; - - private readonly XElement configElement; - - private GraphicsDevice graphicsDevice; - - private ScalableFont defaultFont; - - public ScalableFont Font { get; private set; } - public ScalableFont GlobalFont { get; private set; } - public ScalableFont UnscaledSmallFont { get; private set; } - public ScalableFont SmallFont { get; private set; } - public ScalableFont LargeFont { get; private set; } - public ScalableFont SubHeadingFont { get; private set; } - public ScalableFont DigitalFont { get; private set; } - public ScalableFont HotkeyFont { get; private set; } - public ScalableFont MonospacedFont { get; private set; } - - public Dictionary ForceFontUpperCase + public readonly static ImmutableDictionary Fonts; + public readonly static ImmutableDictionary Sprites; + public readonly static ImmutableDictionary SpriteSheets; + public readonly static ImmutableDictionary Colors; + static GUIStyle() { - get; - private set; - } = new Dictionary(); + var guiClassProperties = typeof(GUIStyle).GetFields(BindingFlags.Public | BindingFlags.Static); - public readonly Sprite[] CursorSprite = new Sprite[7]; + ImmutableDictionary getPropertiesOfType() where T : class + { + return guiClassProperties + .Where(p => p.FieldType == typeof(T)) + .Select(p => (p.Name.ToIdentifier(), p.GetValue(null) as T)) + .ToImmutableDictionary(); + } - public UISprite RadiationSprite { get; private set; } - public SpriteSheet RadiationAnimSpriteSheet { get; private set; } + Fonts = getPropertiesOfType(); + Sprites = getPropertiesOfType(); + SpriteSheets = getPropertiesOfType(); + Colors = getPropertiesOfType(); + } - public SpriteSheet SavingIndicator { get; private set; } + public readonly static PrefabCollection ComponentStyles = new PrefabCollection(); - public UISprite UIGlow { get; private set; } + public readonly static GUIFont Font = new GUIFont("Font"); + public readonly static GUIFont GlobalFont = new GUIFont("GlobalFont"); + public readonly static GUIFont UnscaledSmallFont = new GUIFont("UnscaledSmallFont"); + public readonly static GUIFont SmallFont = new GUIFont("SmallFont"); + public readonly static GUIFont LargeFont = new GUIFont("LargeFont"); + public readonly static GUIFont SubHeadingFont = new GUIFont("SubHeadingFont"); + public readonly static GUIFont DigitalFont = new GUIFont("DigitalFont"); + public readonly static GUIFont HotkeyFont = new GUIFont("HotkeyFont"); + public readonly static GUIFont MonospacedFont = new GUIFont("MonospacedFont"); - public UISprite PingCircle { get; private set; } + public readonly static GUICursor CursorSprite = new GUICursor("Cursor"); - public UISprite YouAreHereCircle { get; private set; } + public readonly static GUISprite SubmarineLocationIcon = new GUISprite("SubmarineLocationIcon"); + public readonly static GUISprite Arrow = new GUISprite("Arrow"); + public readonly static GUISprite SpeechBubbleIcon = new GUISprite("SpeechBubbleIcon"); + public readonly static GUISprite BrokenIcon = new GUISprite("BrokenIcon"); + public readonly static GUISprite YouAreHereCircle = new GUISprite("YouAreHereCircle"); - public UISprite UIGlowCircular { get; private set; } + public readonly static GUISprite Radiation = new GUISprite("Radiation"); + public readonly static GUISpriteSheet RadiationAnimSpriteSheet = new GUISpriteSheet("RadiationAnimSpriteSheet"); - public UISprite UIGlowSolidCircular { get; private set; } - public UISprite UIThermalGlow { get; private set; } + public readonly static GUISpriteSheet SavingIndicator = new GUISpriteSheet("SavingIndicator"); + public readonly static GUISpriteSheet GenericThrobber = new GUISpriteSheet("GenericThrobber"); - public UISprite ButtonPulse { get; private set; } + public readonly static GUISprite UIGlow = new GUISprite("UIGlow"); + public readonly static GUISprite TalentGlow = new GUISprite("TalentGlow"); + public readonly static GUISprite PingCircle = new GUISprite("PingCircle"); + public readonly static GUISprite UIGlowCircular = new GUISprite("UIGlowCircular"); + public readonly static GUISprite UIGlowSolidCircular = new GUISprite("UIGlowSolidCircular"); + public readonly static GUISprite UIThermalGlow = new GUISprite("UIGlowSolidCircular"); + public readonly static GUISprite ButtonPulse = new GUISprite("ButtonPulse"); - public SpriteSheet FocusIndicator { get; private set; } + public readonly static GUISprite EndRoundButtonPulse = new GUISprite("EndRoundButtonPulse"); - public UISprite IconOverflowIndicator { get; private set; } + public readonly static GUISpriteSheet FocusIndicator = new GUISpriteSheet("FocusIndicator"); + + public readonly static GUISprite IconOverflowIndicator = new GUISprite("IconOverflowIndicator"); /// /// General green color used for elements whose colors are set from code /// - public Color Green { get; private set; } = Color.LightGreen; + public readonly static GUIColor Green = new GUIColor("Green"); /// /// General red color used for elements whose colors are set from code /// - public Color Orange { get; private set; } = Color.Orange; + public readonly static GUIColor Orange = new GUIColor("Orange"); /// /// General red color used for elements whose colors are set from code /// - public Color Red { get; private set; } = Color.Red; + public readonly static GUIColor Red = new GUIColor("Red"); /// /// General blue color used for elements whose colors are set from code /// - public Color Blue { get; private set; } = Color.Blue; + public readonly static GUIColor Blue = new GUIColor("Blue"); /// /// General yellow color used for elements whose colors are set from code /// - public Color Yellow { get; private set; } = Color.Yellow; + public readonly static GUIColor Yellow = new GUIColor("Yellow"); - public Color ColorInventoryEmpty { get; private set; } = Color.Red; - public Color ColorInventoryHalf { get; private set; } = Color.Orange; - public Color ColorInventoryFull { get; private set; } = Color.LightGreen; - public Color ColorInventoryBackground { get; private set; } = Color.Gray; - public Color ColorInventoryEmptyOverlay { get; private set; } = Color.Red; + public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty"); + public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf"); + public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull"); + public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground"); + public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay"); - public Color TextColor { get; private set; } = Color.White * 0.8f; - public Color TextColorBright { get; private set; } = Color.White * 0.9f; - public Color TextColorDark { get; private set; } = Color.Black * 0.9f; - public Color TextColorDim { get; private set; } = Color.White * 0.6f; + public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal"); + public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright"); + public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark"); + public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim"); - public Color ItemQualityColorPoor { get; private set; } = Color.DarkRed; - public Color ItemQualityColorNormal { get; private set; } = Color.Gray; - public Color ItemQualityColorGood { get; private set; } = Color.LightGreen; - public Color ItemQualityColorExcellent { get; private set; } = Color.LightBlue; - public Color ItemQualityColorMasterwork { get; private set; } = Color.MediumPurple; - - public Color ColorReputationVeryLow { get; private set; } = Color.Red; - public Color ColorReputationLow { get; private set; } = Color.Orange; - public Color ColorReputationNeutral { get; private set; } = Color.White * 0.8f; - public Color ColorReputationHigh { get; private set; } = Color.LightBlue; - public Color ColorReputationVeryHigh { get; private set; } = Color.Blue; + public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor"); + public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal"); + public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood"); + public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent"); + public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork"); + + public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow"); + public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow"); + public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral"); + public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh"); + public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh"); // Inventory - public Color EquipmentSlotIconColor { get; private set; } = new Color(99, 70, 64); + public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor"); // Health HUD - public Color BuffColorLow { get; private set; } = Color.LightGreen; - public Color BuffColorMedium { get; private set; } = Color.Green; - public Color BuffColorHigh { get; private set; } = Color.DarkGreen; + public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow"); + public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium"); + public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh"); - public Color DebuffColorLow { get; private set; } = Color.DarkSalmon; - public Color DebuffColorMedium { get; private set; } = Color.Red; - public Color DebuffColorHigh { get; private set; } = Color.DarkRed; + public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow"); + public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium"); + public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh"); - public Color HealthBarColorLow { get; private set; } = Color.Red; - public Color HealthBarColorMedium { get; private set; } = Color.Orange; - public Color HealthBarColorHigh { get; private set; } = new Color(78, 114, 88); + public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow"); + public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); + public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public Color EquipmentIndicatorNotEquipped { get; private set; } = Color.Gray; - public Color EquipmentIndicatorEquipped { get; private set; } = new Color(105, 202, 125); - public Color EquipmentIndicatorRunningOut { get; private set; } = new Color(202, 105, 105); + public readonly static GUIColor EquipmentIndicatorNotEquipped = new GUIColor("EquipmentIndicatorNotEquipped"); + public readonly static GUIColor EquipmentIndicatorEquipped = new GUIColor("EquipmentIndicatorEquipped"); + public readonly static GUIColor EquipmentIndicatorRunningOut = new GUIColor("EquipmentIndicatorRunningOut"); public static Point ItemFrameMargin => new Point(50, 56).Multiply(GUI.SlicedSpriteScale); public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); - public GUIStyle(XElement element, GraphicsDevice graphicsDevice) + public static GUIComponentStyle GetComponentStyle(string name) + => ComponentStyles.ContainsKey(name) ? ComponentStyles[name] : null; + + public static void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null) { - this.graphicsDevice = graphicsDevice; - componentStyles = new Dictionary(); - configElement = element; - foreach (XElement subElement in configElement.Elements()) - { - var name = subElement.Name.ToString().ToLowerInvariant(); - switch (name) - { - case "cursor": - if (subElement.HasElements) - { - foreach (var children in subElement.Descendants()) - { - var index = children.GetAttributeInt("state", (int)CursorState.Default); - CursorSprite[index] = new Sprite(children); - } - } - else - { - CursorSprite[(int)CursorState.Default] = new Sprite(subElement); - } - break; - case "green": - Green = subElement.GetAttributeColor("color", Green); - break; - case "orange": - Orange = subElement.GetAttributeColor("color", Orange); - break; - case "red": - Red = subElement.GetAttributeColor("color", Red); - break; - case "blue": - Blue = subElement.GetAttributeColor("color", Blue); - break; - case "yellow": - Yellow = subElement.GetAttributeColor("color", Yellow); - break; - case "colorinventoryempty": - ColorInventoryEmpty = subElement.GetAttributeColor("color", ColorInventoryEmpty); - break; - case "colorinventoryhalf": - ColorInventoryHalf = subElement.GetAttributeColor("color", ColorInventoryHalf); - break; - case "colorinventoryfull": - ColorInventoryFull = subElement.GetAttributeColor("color", ColorInventoryFull); - break; - case "colorinventorybackground": - ColorInventoryBackground = subElement.GetAttributeColor("color", ColorInventoryBackground); - break; - case "colorinventoryemptyoverlay": - ColorInventoryEmptyOverlay = subElement.GetAttributeColor("color", ColorInventoryEmptyOverlay); - break; - case "textcolordark": - TextColorDark = subElement.GetAttributeColor("color", TextColorDark); - break; - case "textcolorbright": - TextColorBright = subElement.GetAttributeColor("color", TextColorBright); - break; - case "textcolordim": - TextColorDim = subElement.GetAttributeColor("color", TextColorDim); - break; - case "textcolornormal": - case "textcolor": - TextColor = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationverylow": - ColorReputationVeryLow = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationlow": - ColorReputationLow = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationneutral": - ColorReputationNeutral = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationhigh": - ColorReputationHigh = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationveryhigh": - ColorReputationVeryHigh = subElement.GetAttributeColor("color", TextColor); - break; - case "equipmentsloticoncolor": - EquipmentSlotIconColor = subElement.GetAttributeColor("color", EquipmentSlotIconColor); - break; - case "buffcolorlow": - BuffColorLow = subElement.GetAttributeColor("color", BuffColorLow); - break; - case "buffcolormedium": - BuffColorMedium = subElement.GetAttributeColor("color", BuffColorMedium); - break; - case "buffcolorhigh": - BuffColorHigh = subElement.GetAttributeColor("color", BuffColorHigh); - break; - case "debuffcolorlow": - DebuffColorLow = subElement.GetAttributeColor("color", DebuffColorLow); - break; - case "debuffcolormedium": - DebuffColorMedium = subElement.GetAttributeColor("color", DebuffColorMedium); - break; - case "debuffcolorhigh": - DebuffColorHigh = subElement.GetAttributeColor("color", DebuffColorHigh); - break; - case "healthbarcolorlow": - HealthBarColorLow = subElement.GetAttributeColor("color", HealthBarColorLow); - break; - case "healthbarcolormedium": - HealthBarColorMedium = subElement.GetAttributeColor("color", HealthBarColorMedium); - break; - case "healthbarcolorhigh": - HealthBarColorHigh = subElement.GetAttributeColor("color", HealthBarColorHigh); - break; - case "equipmentindicatornotequipped": - EquipmentIndicatorNotEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorNotEquipped); - break; - case "equipmentindicatorequipped": - EquipmentIndicatorEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorEquipped); - break; - case "equipmentindicatorrunningout": - EquipmentIndicatorRunningOut = subElement.GetAttributeColor("color", EquipmentIndicatorRunningOut); - break; - case "uiglow": - UIGlow = new UISprite(subElement); - break; - case "pingcircle": - PingCircle = new UISprite(subElement); - break; - case "youareherecircle": - YouAreHereCircle = new UISprite(subElement); - break; - case "radiation": - RadiationSprite = new UISprite(subElement); - break; - case "radiationanimspritesheet": - RadiationAnimSpriteSheet = new SpriteSheet(subElement); - break; - case "uiglowcircular": - UIGlowCircular = new UISprite(subElement); - break; - case "uiglowsolidcircular": - UIGlowSolidCircular = new UISprite(subElement); - break; - case "uithermalglow": - UIThermalGlow = new UISprite(subElement); - break; - case "endroundbuttonpulse": - ButtonPulse = new UISprite(subElement); - break; - case "iconoverflowindicator": - IconOverflowIndicator = new UISprite(subElement); - break; - case "focusindicator": - FocusIndicator = new SpriteSheet(subElement); - break; - case "savingindicator": - SavingIndicator = new SpriteSheet(subElement); - break; - case "font": - Font = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[Font] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "globalfont": - GlobalFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[GlobalFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "unscaledsmallfont": - UnscaledSmallFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[UnscaledSmallFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "smallfont": - SmallFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[SmallFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "largefont": - LargeFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[LargeFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "digitalfont": - DigitalFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[DigitalFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "monospacedfont": - MonospacedFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[MonospacedFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "hotkeyfont": - HotkeyFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[HotkeyFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "objectivetitle": - case "subheading": - SubHeadingFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[SubHeadingFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - default: - GUIComponentStyle componentStyle = new GUIComponentStyle(subElement, this); - componentStyles.Add(subElement.Name.ToString().ToLowerInvariant(), componentStyle); - break; - } - } - - if (GlobalFont == null) - { - GlobalFont = Font; - DebugConsole.NewMessage("Global font not defined in the current UI style file. The global font is used to render western symbols when using Chinese/Japanese/Korean localization. Using default font instead...", Color.Orange); - } - - // TODO: Needs to unregister if we ever remove GUIStyles. - GameMain.Instance.ResolutionChanged += RescaleElements; + Apply(targetComponent, styleName.ToIdentifier(), parent); } - /// - /// Returns the default font of the currently selected language - /// - public ScalableFont LoadCurrentDefaultFont() - { - defaultFont?.Dispose(); - defaultFont = null; - foreach (XElement subElement in configElement.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "font": - defaultFont = LoadFont(subElement, graphicsDevice); - break; - } - } - return defaultFont; - } - - - private void RescaleElements() - { - if (configElement == null) { return; } - if (configElement.Elements() == null) { return; } - foreach (XElement subElement in configElement.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "font": - if (Font == null) { continue; } - Font.Size = GetFontSize(subElement); - break; - case "smallfont": - if (SmallFont == null) { continue; } - SmallFont.Size = GetFontSize(subElement); - break; - case "largefont": - if (LargeFont == null) { continue; } - LargeFont.Size = GetFontSize(subElement); - break; - case "hotkeyfont": - if (HotkeyFont == null) { continue; } - HotkeyFont.Size = GetFontSize(subElement); - break; - case "objectivetitle": - case "subheading": - if (SubHeadingFont == null) { continue; } - SubHeadingFont.Size = GetFontSize(subElement); - break; - } - } - - foreach (var componentStyle in componentStyles.Values) - { - componentStyle.GetSize(componentStyle.Element); - foreach (var childStyle in componentStyle.ChildStyles.Values) - { - childStyle.GetSize(childStyle.Element); - } - } - } - - private ScalableFont LoadFont(XElement element, GraphicsDevice graphicsDevice) - { - string file = GetFontFilePath(element); - uint size = GetFontSize(element); - bool dynamicLoading = GetFontDynamicLoading(element); - bool isCJK = GetIsCJK(element); - return new ScalableFont(file, size, graphicsDevice, dynamicLoading, isCJK); - } - - private uint GetFontSize(XElement element, uint defaultSize = 14) - { - //check if any of the language override fonts want to override the font size as well - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - uint overrideFontSize = GetFontSize(subElement, 0); - if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.TextScale); } - } - } - - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } - Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); - if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) - { - return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.TextScale); - } - } - return (uint)Math.Round(defaultSize * GameSettings.TextScale); - } - - private string GetFontFilePath(XElement element) - { - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - return subElement.GetAttributeString("file", ""); - } - } - return element.GetAttributeString("file", ""); - } - - private bool GetFontDynamicLoading(XElement element) - { - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - return subElement.GetAttributeBool("dynamicloading", false); - } - } - return element.GetAttributeBool("dynamicloading", false); - } - - private bool GetIsCJK(XElement element) - { - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - return subElement.GetAttributeBool("iscjk", false); - } - } - return element.GetAttributeBool("iscjk", false); - } - - public GUIComponentStyle GetComponentStyle(string name) - { - componentStyles.TryGetValue(name.ToLowerInvariant(), out GUIComponentStyle style); - return style; - } - - public void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null) + public static void Apply(GUIComponent targetComponent, Identifier styleName, GUIComponent parent = null) { GUIComponentStyle componentStyle = null; if (parent != null) { GUIComponentStyle parentStyle = parent.Style; - if (parent.Style == null) + if (parentStyle == null) { - string parentStyleName = parent.GetType().Name.ToLowerInvariant(); + Identifier parentStyleName = parent.GetType().Name.ToIdentifier(); - if (!componentStyles.TryGetValue(parentStyleName, out parentStyle)) + if (!ComponentStyles.ContainsKey(parentStyleName)) { - DebugConsole.ThrowError("Couldn't find a GUI style \""+ parentStyleName + "\""); + DebugConsole.ThrowError($"Couldn't find a GUI style \"{parentStyleName}\""); return; } + parentStyle = ComponentStyles[parentStyleName]; } - - string childStyleName = string.IsNullOrEmpty(styleName) ? targetComponent.GetType().Name : styleName; - parentStyle.ChildStyles.TryGetValue(childStyleName.ToLowerInvariant(), out componentStyle); + Identifier childStyleName = styleName.IsEmpty ? targetComponent.GetType().Name.ToIdentifier() : styleName; + parentStyle.ChildStyles.TryGetValue(childStyleName, out componentStyle); } else { - if (string.IsNullOrEmpty(styleName)) + Identifier styleIdentifier = styleName.ToIdentifier(); + if (styleIdentifier == Identifier.Empty) { - styleName = targetComponent.GetType().Name; + styleIdentifier = targetComponent.GetType().Name.ToIdentifier(); } - if (!componentStyles.TryGetValue(styleName.ToLowerInvariant(), out componentStyle)) + if (!ComponentStyles.ContainsKey(styleIdentifier)) { - DebugConsole.ThrowError("Couldn't find a GUI style \""+ styleName+"\""); + DebugConsole.ThrowError($"Couldn't find a GUI style \"{styleIdentifier}\""); return; } + componentStyle = ComponentStyles[styleIdentifier]; } targetComponent.ApplyStyle(componentStyle); } - public Color GetQualityColor(int quality) + public static GUIColor GetQualityColor(int quality) { switch (quality) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 031ec550b..f295fdc0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -7,9 +8,16 @@ using System.Linq; namespace Barotrauma { + public enum ForceUpperCase + { + Inherit, + No, + Yes + } + public class GUITextBlock : GUIComponent { - protected string text; + protected RichString text; protected Alignment textAlignment; @@ -20,10 +28,10 @@ namespace Barotrauma protected Color textColor, disabledTextColor, selectedTextColor; - private string wrappedText; + private LocalizedString wrappedText; private string censoredText; - public delegate string TextGetterHandler(); + public delegate LocalizedString TextGetterHandler(); public TextGetterHandler TextGetter; public bool Wrap; @@ -41,8 +49,6 @@ namespace Barotrauma private float textDepth; - private ScalableFont originalFont; - public Vector2 TextOffset { get; set; } private Vector4 padding; @@ -56,7 +62,7 @@ namespace Barotrauma } } - public override ScalableFont Font + public override GUIFont Font { get { @@ -65,23 +71,25 @@ namespace Barotrauma set { if (base.Font == value) { return; } - base.Font = originalFont = value; - if (text != null && GUI.Style.ForceFontUpperCase.ContainsKey(Font) && GUI.Style.ForceFontUpperCase[Font]) - { - Text = text.ToUpper(); - } + base.Font = value; + if (text != null) { Text = text; } SetTextPos(); } } - public string Text + public RichString Text { get { return text; } set { - string newText = forceUpperCase || (GUI.Style.ForceFontUpperCase.ContainsKey(Font) && GUI.Style.ForceFontUpperCase[Font]) || (style != null && style.ForceUpperCase) ? - value?.ToUpper() : - value; + #warning TODO: Remove this eventually. Nobody should want to pass null. + value ??= ""; + RichString newText = forceUpperCase switch + { + ForceUpperCase.Inherit => value.CaseTiedToFontAndStyle(Font, Style), + ForceUpperCase.No => value.CaseTiedToFontAndStyle(null, null), + ForceUpperCase.Yes => value.ToUpper() + }; if (Text == newText) { return; } @@ -89,21 +97,12 @@ namespace Barotrauma if (autoScaleHorizontal || autoScaleVertical) { textScale = 1.0f; } text = newText; - wrappedText = newText; - if (TextManager.IsCJK(text)) - { - //switch to fallback CJK font - if (!Font.IsCJK) { base.Font = GUI.CJKFont; } - } - else - { - if (Font == GUI.CJKFont) { base.Font = originalFont; } - } + wrappedText = newText.SanitizedString; SetTextPos(); } } - public string WrappedText + public LocalizedString WrappedText { get { return wrappedText; } } @@ -117,7 +116,11 @@ namespace Barotrauma public Vector2 TextPos { get { return textPos; } - set { textPos = value; } + set + { + textPos = value; + ClearCaretPositions(); + } } public float TextScale @@ -169,8 +172,8 @@ namespace Barotrauma } } - private bool forceUpperCase; - public bool ForceUpperCase + private ForceUpperCase forceUpperCase = ForceUpperCase.Inherit; + public ForceUpperCase ForceUpperCase { get { return forceUpperCase; } set @@ -178,12 +181,7 @@ namespace Barotrauma if (forceUpperCase == value) { return; } forceUpperCase = value; - if (forceUpperCase || - (style != null && style.ForceUpperCase) || - (GUI.Style.ForceFontUpperCase.ContainsKey(Font) && GUI.Style.ForceFontUpperCase[Font])) - { - Text = text?.ToUpper(); - } + if (text != null) { Text = text; } } } @@ -247,7 +245,7 @@ namespace Barotrauma public class StrikethroughSettings { - public Color Color { get; set; } = GUI.Style.Red; + public Color Color { get; set; } = GUIStyle.Red; private int thickness; private int expand; @@ -266,13 +264,9 @@ namespace Barotrauma public StrikethroughSettings Strikethrough = null; - public List RichTextData - { - get; - private set; - } + public ImmutableArray? RichTextData => text.RichTextData; - public bool HasColorHighlight => RichTextData != null; + public bool HasColorHighlight => RichTextData.HasValue; public bool OverrideRichTextDataAlpha = true; @@ -292,9 +286,9 @@ namespace Barotrauma /// This is the new constructor. /// If the rectT height is set 0, the height is calculated from the text. /// - public GUITextBlock(RectTransform rectT, string text, Color? textColor = null, ScalableFont font = null, + public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, - bool playerInput = false, bool parseRichText = false) + bool playerInput = false) : base(style, rectT) { if (color.HasValue) @@ -306,28 +300,15 @@ namespace Barotrauma OverrideTextColor(textColor.Value); } - if (parseRichText) - { - RichTextData = Barotrauma.RichTextData.GetRichTextData(text, out text); - if (RichTextData != null && RichTextData.Count == 0) - { - RichTextData = null; - } - } - //if the text is in chinese/korean/japanese and we're not using a CJK-compatible font, //use the default CJK font as a fallback - var selectedFont = originalFont = font ?? GUI.Font; - if (TextManager.IsCJK(text) && !selectedFont.IsCJK) - { - selectedFont = GUI.CJKFont; - } + var selectedFont = font ?? GUIStyle.Font; this.Font = selectedFont; this.textAlignment = textAlignment; this.Wrap = wrap; this.Text = text ?? ""; this.playerInput = playerInput; - if (rectT.Rect.Height == 0 && !string.IsNullOrEmpty(text)) + if (rectT.Rect.Height == 0 && !text.IsNullOrEmpty()) { CalculateHeightFromText(); } @@ -339,11 +320,6 @@ namespace Barotrauma Enabled = true; Censor = false; } - public GUITextBlock(RectTransform rectT, List richTextData, string text, Color? textColor = null, ScalableFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) - : this(rectT, text, textColor, font, textAlignment, wrap, style, color, playerInput) - { - this.RichTextData = richTextData; - } public void CalculateHeightFromText(int padding = 0, bool removeExtraSpacing = false) { @@ -351,10 +327,9 @@ namespace Barotrauma RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(wrappedText, removeExtraSpacing).Y + padding)); } - public void SetRichText(string richText) + public void SetRichText(LocalizedString richText) { - RichTextData = Barotrauma.RichTextData.GetRichTextData(richText, out string sanitizedText); - Text = sanitizedText; + Text = RichString.Rich(richText); } public override void ApplyStyle(GUIComponentStyle componentStyle) @@ -368,41 +343,34 @@ namespace Barotrauma disabledTextColor = componentStyle.DisabledTextColor; selectedTextColor = componentStyle.SelectedTextColor; - switch (componentStyle.Font) + if (Font == null || !componentStyle.Font.IsEmpty) { - case "font": - Font = componentStyle.Style.Font; - break; - case "smallfont": - Font = componentStyle.Style.SmallFont; - break; - case "largefont": - Font = componentStyle.Style.LargeFont; - break; - case "objectivetitle": - case "subheading": - Font = componentStyle.Style.SubHeadingFont; - break; + Font = GUIStyle.Fonts[componentStyle.Font.AppendIfMissing("Font")]; } } + + public void ClearCaretPositions() + { + cachedCaretPositions = ImmutableArray.Empty; + } public void SetTextPos() { - cachedCaretPositions = ImmutableArray.Empty; + ClearCaretPositions(); if (text == null) { return; } - censoredText = string.IsNullOrEmpty(text) ? "" : new string('\u2022', text.Length); + censoredText = text.IsNullOrEmpty() ? "" : new string('\u2022', text.Length); var rect = Rect; overflowClipActive = false; - wrappedText = text; + wrappedText = text.SanitizedString; - TextSize = MeasureText(text); + TextSize = MeasureText(text.SanitizedString); if (Wrap && rect.Width > 0) { - wrappedText = ToolBox.WrapText(text, rect.Width - padding.X - padding.Z, Font, textScale); + wrappedText = ToolBox.WrapText(text.SanitizedString, rect.Width - padding.X - padding.Z, Font, textScale); TextSize = MeasureText(wrappedText); } else if (OverflowClip) @@ -426,15 +394,15 @@ namespace Barotrauma textPos = new Vector2(padding.X + (rect.Width - padding.Z - padding.X) / 2.0f, padding.Y + (rect.Height - padding.Y - padding.W) / 2.0f); origin = TextSize * 0.5f; + origin.X = 0; if (textAlignment.HasFlag(Alignment.Left) && !overflowClipActive) { textPos.X = padding.X; - origin.X = 0; } if (textAlignment.HasFlag(Alignment.Right) || overflowClipActive) { textPos.X = rect.Width - padding.Z; - origin.X = TextSize.X; + //origin.X = TextSize.X; } if (textAlignment.HasFlag(Alignment.Top)) { @@ -454,7 +422,12 @@ namespace Barotrauma textPos.Y = (int)textPos.Y; } - private Vector2 MeasureText(string text) + private Vector2 MeasureText(LocalizedString text) + { + return MeasureText(text.Value); + } + + private Vector2 MeasureText(string text) { if (Font == null) return Vector2.Zero; @@ -498,12 +471,20 @@ namespace Barotrauma { return cachedCaretPositions; } - string textDrawn = Censor ? CensoredText : Text; + string textDrawn = Censor ? CensoredText : Text.SanitizedValue; float w = Wrap ? (Rect.Width - Padding.X - Padding.Z) / TextScale : float.PositiveInfinity; - Font.WrapText(textDrawn, w, out Vector2[] positions); - cachedCaretPositions = positions.Select(p => p * TextScale + TextPos - Origin * TextScale).ToImmutableArray(); + string wrapped = Font.WrapText(textDrawn, w, out Vector2[] positions); + int textWidth = (int)Font.MeasureString(wrapped).X; + int alignmentXDiff + = textAlignment.HasFlag(Alignment.Right) ? textWidth + : textAlignment.HasFlag(Alignment.Center) ? textWidth / 2 + : 0; + cachedCaretPositions = positions + .Select(p => p - new Vector2(alignmentXDiff, 0)) + .Select(p => p * TextScale + TextPos - Origin * TextScale) + .ToImmutableArray(); return cachedCaretPositions; } @@ -584,7 +565,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - if (!string.IsNullOrEmpty(text)) + if (!text.IsNullOrEmpty()) { Vector2 pos = rect.Location.ToVector2() + textPos + TextOffset; if (RoundToNearestPixel) @@ -605,28 +586,29 @@ namespace Barotrauma if (!HasColorHighlight) { - string textToShow = Censor ? censoredText : (Wrap ? wrappedText : text); + string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f); if (Shadow) { Vector2 shadowOffset = new Vector2(GUI.IntScale(2)); - Font.DrawString(spriteBatch, textToShow, pos + shadowOffset, Color.Black, 0.0f, origin, TextScale, SpriteEffects.None, textDepth); + Font.DrawString(spriteBatch, textToShow, pos + shadowOffset, Color.Black, 0.0f, origin, TextScale, SpriteEffects.None, textDepth, alignment: textAlignment, forceUpperCase: ForceUpperCase); } - Font.DrawString(spriteBatch, textToShow, pos, colorToShow, 0.0f, origin, TextScale, SpriteEffects.None, textDepth); + Font.DrawString(spriteBatch, textToShow, pos, colorToShow, 0.0f, origin, TextScale, SpriteEffects.None, textDepth, alignment: textAlignment, forceUpperCase: ForceUpperCase); } else { if (OverrideRichTextDataAlpha) { - RichTextData.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); + RichTextData.Value.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); } - Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text), pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData); + Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text.SanitizedString).Value, pos, + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData.Value, alignment: textAlignment, forceUpperCase: ForceUpperCase); } - Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, ForceUpperCase ? pos.Y : pos.Y + GUI.Scale * 2f); + Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, + /* TODO: ???? */ForceUpperCase == ForceUpperCase.Yes ? pos.Y : pos.Y + GUI.Scale * 2f); } if (overflowClipActive) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 147a24e37..7a096a1a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -66,9 +66,6 @@ namespace Barotrauma private int selectionStartIndex; private int selectionEndIndex; private bool IsLeftToRight => selectionStartIndex <= selectionEndIndex; - private Vector2 selectionStartPos; - private Vector2 selectionEndPos; - private Vector2 selectionRectSize; private GUICustomComponent caretAndSelectionRenderer; @@ -141,7 +138,7 @@ namespace Barotrauma maxTextLength = value; if (Text.Length > MaxTextLength) { - SetText(textBlock.Text.Substring(0, (int)maxTextLength)); + SetText(Text.Substring(0, (int)maxTextLength)); } } } @@ -172,7 +169,7 @@ namespace Barotrauma set { textBlock.Censor = value; } } - public override string ToolTip + public override RichString ToolTip { get { @@ -184,7 +181,7 @@ namespace Barotrauma } } - public override ScalableFont Font + public override GUIFont Font { get { return textBlock?.Font ?? base.Font; } set @@ -237,7 +234,7 @@ namespace Barotrauma { get { - return textBlock.Text; + return textBlock.Text.SanitizedValue; } set { @@ -249,12 +246,12 @@ namespace Barotrauma public string WrappedText { - get { return textBlock.WrappedText; } + get { return textBlock.WrappedText.Value; } } public bool Readonly { get; set; } - public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, ScalableFont font = null, + public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) { @@ -263,9 +260,10 @@ namespace Barotrauma this.color = color ?? Color.White; frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); - GUI.Style.Apply(frame, style == "" ? "GUITextBox" : style); - textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text, textColor, font, textAlignment, wrap, playerInput: true); - GUI.Style.Apply(textBlock, "", this); + GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style); + textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap, playerInput: true); + GUIStyle.Apply(textBlock, "", this); + if (font != null) { textBlock.Font = font; } CaretEnabled = true; caretPosDirty = true; @@ -287,10 +285,11 @@ namespace Barotrauma clearButtonWidth = (int)(clearButton.Rect.Width * 1.2f); } - if (this.style != null && this.style.ChildStyles.ContainsKey("textboxicon") && createPenIcon) + var selfStyle = Style; + if (selfStyle != null && selfStyle.ChildStyles.ContainsKey("textboxicon".ToIdentifier()) && createPenIcon) { icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5 + clearButtonWidth, 0) }, null, scaleToFit: true); - icon.ApplyStyle(this.style.ChildStyles["textboxicon"]); + icon.ApplyStyle(this.Style.ChildStyles["textboxicon".ToIdentifier()]); textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - clearButtonWidth - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } Font = textBlock.Font; @@ -315,53 +314,38 @@ namespace Barotrauma { text = textFilterFunction(text); } - if (textBlock.Text == text) { return false; } + if (Text == text) { return false; } textBlock.Text = text; - if (textBlock.Text == null) textBlock.Text = ""; - if (textBlock.Text != "" && !Wrap) + if (Text == null) textBlock.Text = ""; + if (Text != "" && !Wrap) { if (maxTextLength != null) { if (textBlock.Text.Length > maxTextLength) { - textBlock.Text = textBlock.Text.Substring(0, (int)maxTextLength); + textBlock.Text = Text.Substring(0, (int)maxTextLength); } } else { while (ClampText && textBlock.Text.Length > 0 && Font.MeasureString(textBlock.Text).X * TextBlock.TextScale > (int)(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z)) { - textBlock.Text = textBlock.Text.Substring(0, textBlock.Text.Length - 1); + textBlock.Text = Text.Substring(0, textBlock.Text.Length - 1); } } } if (store) { - memento.Store(textBlock.Text); + memento.Store(Text); } return true; } private void CalculateCaretPos() { - if (Censor || !Wrap) - { - string textDrawn = textBlock.CensoredText; - CaretIndex = Math.Min(CaretIndex, textDrawn.Length); - textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; - Vector2 textSize = Font.MeasureString(textDrawn[..CaretIndex]) * TextBlock.TextScale; - caretPos = new Vector2(textSize.X, 0) + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; - } - else - { - CaretIndex = Math.Min(CaretIndex, textBlock.Text.Length); - textBlock.Font.WrapText( - textBlock.Text, - (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, - CaretIndex, - out Vector2 requestedCharPos); - caretPos = requestedCharPos * TextBlock.TextScale + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; - } + CaretIndex = Math.Clamp(CaretIndex, 0, textBlock.Text.Length); + var caretPositions = textBlock.GetAllCaretPositions(); + caretPos = caretPositions[CaretIndex]; caretPosDirty = false; } @@ -460,14 +444,19 @@ namespace Barotrauma { if (textBlock.OverflowClipActive) { - if (CaretScreenPos.X < textBlock.Rect.X + textBlock.Padding.X) + float left = textBlock.Rect.X + textBlock.Padding.X; + if (CaretScreenPos.X < left) { - textBlock.TextPos = new Vector2(textBlock.TextPos.X + ((textBlock.Rect.X + textBlock.Padding.X) - CaretScreenPos.X), textBlock.TextPos.Y); + float diff = left - CaretScreenPos.X; + textBlock.TextPos = new Vector2(textBlock.TextPos.X + diff, textBlock.TextPos.Y); CalculateCaretPos(); } - else if (CaretScreenPos.X > textBlock.Rect.Right - textBlock.Padding.Z) + + float right = textBlock.Rect.Right - textBlock.Padding.Z; + if (CaretScreenPos.X > right) { - textBlock.TextPos = new Vector2(textBlock.TextPos.X - (CaretScreenPos.X - (textBlock.Rect.Right - textBlock.Padding.Z)), textBlock.TextPos.Y); + float diff = CaretScreenPos.X - right; + textBlock.TextPos = new Vector2(textBlock.TextPos.X - diff, textBlock.TextPos.Y); CalculateCaretPos(); } } @@ -499,74 +488,54 @@ namespace Barotrauma private void DrawCaretAndSelection(SpriteBatch spriteBatch, GUICustomComponent customComponent) { if (!Visible) { return; } - if (Selected) + if (!Selected) { return; } + + if (caretVisible) { - if (caretVisible ) - { - GUI.DrawLine(spriteBatch, - new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + 3), - new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + Font.MeasureString("I").Y * textBlock.TextScale - 3), - CaretColor ?? textBlock.TextColor * (textBlock.TextColor.A / 255.0f)); - } - if (selectedCharacters > 0) - { - DrawSelectionRect(spriteBatch); - } - //GUI.DrawString(spriteBatch, new Vector2(100, 0), selectedCharacters.ToString(), Color.LightBlue, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 20), selectionStartIndex.ToString(), Color.White, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(140, 20), selectionEndIndex.ToString(), Color.White, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 40), selectedText.ToString(), Color.Yellow, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 60), $"caret index: {CaretIndex.ToString()}", GUI.Style.Red, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 80), $"caret pos: {caretPos.ToString()}", GUI.Style.Red, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 100), $"caret screen pos: {CaretScreenPos.ToString()}", GUI.Style.Red, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 120), $"text start pos: {(textBlock.TextPos - textBlock.Origin).ToString()}", Color.White, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 140), $"cursor pos: {PlayerInput.MousePosition.ToString()}", Color.White, Color.Black); + GUI.DrawLine(spriteBatch, + new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + 3), + new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + Font.LineHeight * textBlock.TextScale - 3), + CaretColor ?? textBlock.TextColor * (textBlock.TextColor.A / 255.0f)); + } + if (selectedCharacters > 0) + { + DrawSelectionRect(spriteBatch); } } private void DrawSelectionRect(SpriteBatch spriteBatch) { - if (textBlock.WrappedText.Contains("\n")) - { - // Multiline selection - var characterPositions = textBlock.GetAllCaretPositions(); - (int startIndex, int endIndex) = selectionStartIndex < selectionEndIndex - ? (selectionStartIndex, selectionEndIndex) - : (selectionEndIndex, selectionStartIndex); - endIndex--; + var characterPositions = textBlock.GetAllCaretPositions(); + (int startIndex, int endIndex) = IsLeftToRight + ? (selectionStartIndex, selectionEndIndex) + : (selectionEndIndex, selectionStartIndex); + endIndex--; - void drawRect(Vector2 topLeft, Vector2 bottomRight) - { - int minWidth = GUI.IntScale(5); - if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; } - GUI.DrawRectangle(spriteBatch, - Rect.Location.ToVector2() + topLeft, - bottomRight - topLeft, - SelectionColor, isFilled: true); - } - - Vector2 topLeft = characterPositions[startIndex]; - for (int i = startIndex+1; i <= endIndex; i++) - { - Vector2 currPos = characterPositions[i]; - if (!MathUtils.NearlyEqual(topLeft.Y, currPos.Y)) - { - Vector2 bottomRight = characterPositions[i - 1]; - bottomRight += Font.MeasureChar(Text[i - 1]); - drawRect(topLeft, bottomRight); - topLeft = currPos; - } - } - Vector2 finalBottomRight = characterPositions[endIndex]; - finalBottomRight += Font.MeasureChar(Text[endIndex]); - drawRect(topLeft, finalBottomRight); - } - else + void drawRect(Vector2 topLeft, Vector2 bottomRight) { - // Single line selection - Vector2 topLeft = IsLeftToRight ? selectionStartPos : selectionEndPos; - GUI.DrawRectangle(spriteBatch, Rect.Location.ToVector2() + topLeft, selectionRectSize, SelectionColor, isFilled: true); + int minWidth = GUI.IntScale(5); + if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; } + GUI.DrawRectangle(spriteBatch, + Rect.Location.ToVector2() + topLeft, + bottomRight - topLeft, + SelectionColor, isFilled: true); } + + Vector2 topLeft = characterPositions[startIndex]; + for (int i = startIndex+1; i <= endIndex; i++) + { + Vector2 currPos = characterPositions[i]; + if (!MathUtils.NearlyEqual(topLeft.Y, currPos.Y)) + { + Vector2 bottomRight = characterPositions[i - 1]; + bottomRight += Font.MeasureChar(Text[i - 1]) * TextBlock.TextScale; + drawRect(topLeft, bottomRight); + topLeft = currPos; + } + } + Vector2 finalBottomRight = characterPositions[endIndex]; + finalBottomRight += Font.MeasureChar(Text[endIndex]) * TextBlock.TextScale; + drawRect(topLeft, finalBottomRight); } public void ReceiveTextInput(char inputChar) @@ -700,8 +669,8 @@ namespace Barotrauma float lineHeight = Font.LineHeight * TextBlock.TextScale; int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y - lineHeight * 0.5f)); textBlock.Font.WrapText( - textBlock.Text, - (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, + textBlock.Text.SanitizedValue, + GetWrapWidth(), newIndex, out Vector2 requestedCharPos); requestedCharPos *= TextBlock.TextScale; @@ -718,8 +687,8 @@ namespace Barotrauma lineHeight = Font.LineHeight * TextBlock.TextScale; newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y + lineHeight * 1.5f)); textBlock.Font.WrapText( - textBlock.Text, - (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, + textBlock.Text.SanitizedValue, + GetWrapWidth(), newIndex, out Vector2 requestedCharPos2); requestedCharPos2 *= TextBlock.TextScale; @@ -806,7 +775,6 @@ namespace Barotrauma { CaretIndex = 0; CalculateCaretPos(); - selectionStartPos = caretPos; selectionStartIndex = 0; CaretIndex = Text.Length; CalculateSelection(); @@ -846,6 +814,9 @@ namespace Barotrauma OnTextChanged?.Invoke(this, Text); } + private float GetWrapWidth() + => Wrap ? (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale : float.PositiveInfinity; + private void InitSelectionStart() { if (caretPosDirty) @@ -855,29 +826,20 @@ namespace Barotrauma if (selectionStartIndex == -1) { selectionStartIndex = CaretIndex; - selectionStartPos = caretPos; } } private void CalculateSelection() { - string textDrawn = Censor ? textBlock.CensoredText : textBlock.WrappedText; + string textDrawn = Censor ? textBlock.CensoredText : WrappedText; InitSelectionStart(); selectionEndIndex = Math.Min(CaretIndex, textDrawn.Length); - selectionEndPos = caretPos; selectedCharacters = Math.Abs(selectionStartIndex - selectionEndIndex); try { - if (IsLeftToRight) - { - selectedText = Text.Substring(selectionStartIndex, Math.Min(selectedCharacters, Text.Length)); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionStartIndex, Math.Min(selectedCharacters, textDrawn.Length))) * TextBlock.TextScale; - } - else - { - selectedText = Text.Substring(selectionEndIndex, Math.Min(selectedCharacters, Text.Length)); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionEndIndex, Math.Min(selectedCharacters, textDrawn.Length))) * TextBlock.TextScale; - } + selectedText = Text.Substring( + IsLeftToRight ? selectionStartIndex : selectionEndIndex, + Math.Min(selectedCharacters, Text.Length)); } catch (ArgumentOutOfRangeException exception) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 2d589dc6b..05e59d5fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -20,18 +20,18 @@ namespace Barotrauma public override bool Selected { - get { return selected; } + get { return isSelected; } set { - if (value == selected) { return; } + if (value == isSelected) { return; } if (radioButtonGroup != null && radioButtonGroup.SelectedRadioButton == this) { - selected = true; + isSelected = true; return; } - selected = value; - State = selected ? ComponentState.Selected : ComponentState.None; + isSelected = value; + State = isSelected ? ComponentState.Selected : ComponentState.None; if (value && radioButtonGroup != null) { radioButtonGroup.SelectRadioButton(this); @@ -88,7 +88,7 @@ namespace Barotrauma } }*/ - public override ScalableFont Font + public override GUIFont Font { get { @@ -112,7 +112,7 @@ namespace Barotrauma get { return text; } } - public override string ToolTip + public override RichString ToolTip { get { return base.ToolTip; } set @@ -123,13 +123,13 @@ namespace Barotrauma } } - public string Text + public LocalizedString Text { get { return text.Text; } set { text.Text = value; } } - public GUITickBox(RectTransform rectT, string label, ScalableFont font = null, string style = "") : base(null, rectT) + public GUITickBox(RectTransform rectT, LocalizedString label, GUIFont font = null, string style = "") : base(null, rectT) { CanBeFocused = true; HoverCursor = CursorState.Hand; @@ -145,7 +145,7 @@ namespace Barotrauma SelectedColor = Color.DarkGray, CanBeFocused = false }; - GUI.Style.Apply(box, style == "" ? "GUITickBox" : style); + GUIStyle.Apply(box, style == "" ? "GUITickBox" : style); if (box.RectTransform.MinSize.Y > 0) { RectTransform.MinSize = box.RectTransform.MinSize; @@ -159,7 +159,7 @@ namespace Barotrauma { CanBeFocused = false }; - GUI.Style.Apply(text, "GUITextBlock", this); + GUIStyle.Apply(text, "GUITextBlock", this); Enabled = true; ResizeBox(); @@ -205,13 +205,13 @@ namespace Barotrauma { Selected = !Selected; } - else if (!selected) + else if (!isSelected) { Selected = true; } } } - else if (selected) + else if (isSelected) { State = ComponentState.Selected; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 69f18c269..ad3b9c4d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -90,7 +90,8 @@ namespace Barotrauma if (GameMain.Instance != null) { GameMain.Instance.ResolutionChanged += CreateAreas; - GameMain.Config.OnHUDScaleChanged += CreateAreas; + #warning TODO: reimplement + //GameSettings.CurrentConfig.OnHUDScaleChanged += CreateAreas; CreateAreas(); CharacterInfo.Init(); } @@ -163,7 +164,7 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch) { GUI.DrawRectangle(spriteBatch, ButtonAreaTop, Color.White * 0.5f); - GUI.DrawRectangle(spriteBatch, MessageAreaTop, GUI.Style.Orange * 0.5f); + GUI.DrawRectangle(spriteBatch, MessageAreaTop, GUIStyle.Orange * 0.5f); GUI.DrawRectangle(spriteBatch, CrewArea, Color.Blue * 0.5f); GUI.DrawRectangle(spriteBatch, ChatBoxArea, Color.Cyan * 0.5f); GUI.DrawRectangle(spriteBatch, HealthBarArea, Color.Red * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 8d83781a8..35bcab726 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Media; using System.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -69,14 +70,10 @@ namespace Barotrauma } } - private string selectedTip; - private List selectedTipRichTextData; - private bool selectedTipRichTextUnparsed; - private void SetSelectedTip(string tip) + private RichString selectedTip; + private void SetSelectedTip(LocalizedString tip) { - selectedTip = tip; - selectedTipRichTextData = null; - selectedTipRichTextUnparsed = true; + selectedTip = RichString.Rich(tip); } private readonly object loadMutex = new object(); @@ -113,6 +110,8 @@ namespace Barotrauma set; } + public LanguageIdentifier[] AvailableLanguages = null; + public LoadingScreen(GraphicsDevice graphics) { defaultBackgroundTexture = TextureLoader.FromFile("Content/Map/LocationPortraits/AlienRuins.png"); @@ -123,12 +122,12 @@ namespace Barotrauma overlay = TextureLoader.FromFile("Content/UI/LoadingScreenOverlay.png"); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; - SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); + SetSelectedTip(TextManager.Get("LoadingScreenTip")); } public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphics, float deltaTime) { - if (GameMain.Config.EnableSplashScreen) + if (GameSettings.CurrentConfig.EnableSplashScreen) { try { @@ -138,11 +137,11 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Playing splash screen video failed", e); - GameMain.Config.EnableSplashScreen = false; + DisableSplashScreen(); } } - - var titleStyle = GUI.Style?.GetComponentStyle("TitleText"); + + var titleStyle = GUIStyle.GetComponentStyle("TitleText"); Sprite titleSprite = null; if (!WaitForLanguageSelection && titleStyle != null && titleStyle.Sprites.ContainsKey(GUIComponent.ComponentState.None)) { @@ -187,67 +186,58 @@ namespace Barotrauma } else if (DrawLoadingText) { - if (TextManager.Initialized) + LocalizedString loadText; + if (LoadState == 100.0f) { - string loadText; - if (LoadState == 100.0f) +#if DEBUG + if (GameSettings.CurrentConfig.AutomaticQuickStartEnabled || GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled || (GameSettings.CurrentConfig.TestScreenEnabled && GameMain.FirstLoad)) { -#if DEBUG - if (GameMain.Config.AutomaticQuickStartEnabled || GameMain.Config.AutomaticCampaignLoadEnabled || GameMain.Config.TestScreenEnabled && GameMain.FirstLoad) - { - loadText = "QUICKSTARTING ..."; - } - else - { -#endif - loadText = TextManager.Get("PressAnyKey"); -#if DEBUG - } -#endif + loadText = "QUICKSTARTING ..."; } else { - loadText = TextManager.Get("Loading"); - if (LoadState != null) - { - loadText += " " + (int)LoadState + " %"; +#endif + loadText = TextManager.Get("PressAnyKey"); +#if DEBUG + } +#endif + } + else + { + loadText = TextManager.Get("Loading"); + if (LoadState != null) + { + loadText += " " + (int)LoadState + " %"; #if DEBUG - if (GameMain.FirstLoad && GameMain.CancelQuickStart) - { - loadText += " (Quickstart aborted)"; - } -#endif + if (GameMain.FirstLoad && GameMain.CancelQuickStart) + { + loadText += " (Quickstart aborted)"; } - } - if (GUI.LargeFont != null) - { - GUI.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), - new Vector2(GameMain.GraphicsWidth / 2.0f - GUI.LargeFont.MeasureString(loadText.ToUpper()).X / 2.0f, GameMain.GraphicsHeight * 0.75f), - Color.White); +#endif } } - - if (GUI.Font != null && selectedTip != null) + if (GUIStyle.LargeFont.HasValue) { - if (selectedTipRichTextUnparsed) - { - selectedTipRichTextData = RichTextData.GetRichTextData(selectedTip, out selectedTip); - selectedTipRichTextUnparsed = false; - } + GUIStyle.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), + new Vector2(GameMain.GraphicsWidth / 2.0f - GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).X / 2.0f, GameMain.GraphicsHeight * 0.75f), + Color.White); + } - string wrappedTip = ToolBox.WrapText(selectedTip, GameMain.GraphicsWidth * 0.5f, GUI.Font); + if (GUIStyle.Font.HasValue && selectedTip != null) + { + string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.5f, GUIStyle.Font.Value); string[] lines = wrappedTip.Split('\n'); - float lineHeight = GUI.Font.MeasureString(selectedTip).Y; + float lineHeight = GUIStyle.Font.MeasureString(selectedTip).Y; - if (selectedTipRichTextData != null) + if (selectedTip.RichTextData != null) { int rtdOffset = 0; for (int i = 0; i < lines.Length; i++) { - GUI.Font.DrawStringWithColors(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUI.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, - 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTipRichTextData, rtdOffset); + GUIStyle.Font.DrawStringWithColors(spriteBatch, lines[i], + new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, + 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTip.RichTextData.Value, rtdOffset); rtdOffset += lines[i].Length; } } @@ -255,8 +245,8 @@ namespace Barotrauma { for (int i = 0; i < lines.Length; i++) { - GUI.Font.DrawString(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUI.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); + GUIStyle.Font.DrawString(spriteBatch, lines[i], + new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); } } } @@ -280,7 +270,7 @@ namespace Barotrauma if (noiseVal < 0.2f) { //SCP-CB reference - randText = (new string[] { "NIL", "black white gray", "Sometimes we would have had time to scream", "e8m106]af", "NO" }).GetRandom(); + randText = (new string[] { "NIL", "black white gray", "Sometimes we would have had time to scream", "e8m106]af", "NO" }).GetRandomUnsynced(); } else if (noiseVal < 0.3f) { @@ -295,15 +285,20 @@ namespace Barotrauma Rand.Int(100).ToString().PadLeft(2, '0'); } - GUI.LargeFont?.DrawString(spriteBatch, randText, - new Vector2(GameMain.GraphicsWidth - decorativeMap.FrameSize.X * decorativeScale.X * 0.8f, GameMain.GraphicsHeight * 0.57f), - Color.White * (1.0f - noiseVal)); + if (GUIStyle.LargeFont.HasValue) + { + GUIStyle.LargeFont.DrawString(spriteBatch, randText, + new Vector2(GameMain.GraphicsWidth - decorativeMap.FrameSize.X * decorativeScale.X * 0.8f, GameMain.GraphicsHeight * 0.57f), + Color.White * (1.0f - noiseVal)); + } spriteBatch.End(); } private void DrawLanguageSelectionPrompt(SpriteBatch spriteBatch, GraphicsDevice graphicsDevice) { + if (AvailableLanguages is null) { return; } + if (languageSelectionFont == null) { languageSelectionFont = new ScalableFont("Content/Fonts/NotoSans/NotoSans-Bold.ttf", @@ -320,8 +315,8 @@ namespace Barotrauma } Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.3f); - Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / TextManager.AvailableLanguages.Count()); - foreach (string language in TextManager.AvailableLanguages) + Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / AvailableLanguages.Length); + foreach (LanguageIdentifier language in AvailableLanguages) { string localizedLanguageName = TextManager.GetTranslatedLanguageName(language); var font = TextManager.IsCJK(localizedLanguageName) ? languageSelectionFontCJK : languageSelectionFont; @@ -335,11 +330,11 @@ namespace Barotrauma hover ? Color.White : Color.White * 0.6f); if (hover && PlayerInput.PrimaryMouseButtonClicked()) { - GameMain.Config.Language = language; + var config = GameSettings.CurrentConfig; + config.Language = language; + GameSettings.SetCurrentConfig(config); //reload tip in the selected language - SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); - GameMain.Config.SetDefaultBindings(legacy: false); - GameMain.Config.CheckBindings(useDefaults: true); + SetSelectedTip(TextManager.Get("LoadingScreenTip")); WaitForLanguageSelection = false; languageSelectionFont?.Dispose(); languageSelectionFont = null; languageSelectionFontCJK?.Dispose(); languageSelectionFontCJK = null; @@ -368,7 +363,7 @@ namespace Barotrauma } catch (Exception e) { - GameMain.Config.EnableSplashScreen = false; + DisableSplashScreen(); DebugConsole.ThrowError("Playing the splash screen \"" + fileName + "\" failed.", e); PendingSplashScreens.Clear(); currSplashScreen = null; @@ -425,13 +420,20 @@ namespace Barotrauma } } + private void DisableSplashScreen() + { + var config = GameSettings.CurrentConfig; + config.EnableSplashScreen = false; + GameSettings.SetCurrentConfig(config); + } + bool drawn; public IEnumerable DoLoading(IEnumerable loader) { drawn = false; LoadState = null; - SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); - currentBackgroundTexture = LocationType.List.GetRandom()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; + SetSelectedTip(TextManager.Get("LoadingScreenTip")); + currentBackgroundTexture = LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 4596b3c7e..fb87b3639 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -50,7 +50,7 @@ namespace Barotrauma Afflictions = new List(); } - public PendingAfflictionElement? FindAfflictionElement(MedicalClinic.NetAffliction target) => Afflictions.FirstOrNull(element => element.Target.Identifier.Equals(target.Identifier, StringComparison.OrdinalIgnoreCase)); + public PendingAfflictionElement? FindAfflictionElement(MedicalClinic.NetAffliction target) => Afflictions.FirstOrNull(element => element.Target.Identifier == target.Identifier); } // Represents an affliction on the left side crew entry @@ -269,11 +269,11 @@ namespace Barotrauma int totalCost = medicalClinic.GetTotalCost(); healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); - healList.PriceBlock.TextColor = GUI.Style.Red; + healList.PriceBlock.TextColor = GUIStyle.Red; healList.HealButton.Enabled = false; if (medicalClinic.GetMoney() > totalCost) { - healList.PriceBlock.TextColor = GUI.Style.TextColor; + healList.PriceBlock.TextColor = GUIStyle.TextColorNormal; if (medicalClinic.PendingHeals.Any()) { healList.HealButton.Enabled = true; @@ -443,7 +443,7 @@ namespace Barotrauma GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); - GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUI.LargeFont); + GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform)); @@ -459,13 +459,13 @@ namespace Barotrauma }; GUILayoutGroup balanceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), crewContent.RectTransform)); - GUITextBlock balanceLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), TextManager.Get("campaignstore.balance"), textAlignment: Alignment.BottomRight, font: GUI.Font) + GUITextBlock balanceLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), TextManager.Get("campaignstore.balance"), textAlignment: Alignment.BottomRight, font: GUIStyle.Font) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUI.Style.SubHeadingFont) + GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetMoney()), AutoScaleVertical = true, @@ -519,18 +519,18 @@ namespace Barotrauma GUILayoutGroup healthLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.1f, 1f), crewLayout.RectTransform), isHorizontal: true, Anchor.Center); - new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { TextGetter = () => $"{(int)(info.Character?.HealthPercentage ?? 100f)}%", - TextColor = GUI.Style.Green + TextColor = GUIStyle.Green }; GUITextBlock overflowIndicator = - new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), afflictionList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), text: "+", textAlignment: Alignment.Center, font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), afflictionList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), text: "+", textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { Visible = false, CanBeFocused = false, - TextColor = GUI.Style.Red + TextColor = GUIStyle.Red }; MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty() }; @@ -552,13 +552,13 @@ namespace Barotrauma Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), pendingHealContainer.RectTransform), TextManager.Get("medicalclinic.pendingheals"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), pendingHealContainer.RectTransform), TextManager.Get("medicalclinic.pendingheals"), font: GUIStyle.SubHeadingFont); GUIFrame healListContainer = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), pendingHealContainer.RectTransform), style: null); GUITextBlock? errorBlock = null; if (!GameMain.IsSingleplayer) { - errorBlock = new GUITextBlock(new RectTransform(Vector2.One, healListContainer.RectTransform), text: TextManager.Get("pleasewaitupnp"), font: GUI.LargeFont, textAlignment: Alignment.Center); + errorBlock = new GUITextBlock(new RectTransform(Vector2.One, healListContainer.RectTransform), text: TextManager.Get("pleasewaitupnp"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center); } GUIListBox healList = new GUIListBox(new RectTransform(Vector2.One, healListContainer.RectTransform)) @@ -571,7 +571,7 @@ namespace Barotrauma GUILayoutGroup priceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true); GUITextBlock priceLabelBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.Get("campaignstore.total")); - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUI.SubHeadingFont, + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); @@ -679,12 +679,12 @@ namespace Barotrauma GUILayoutGroup textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentLayout.RectTransform), isHorizontal: true); - string name = prefab.Name; + LocalizedString name = prefab.Name; GUIFrame textContainer = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), textLayout.RectTransform), style: null); - GUITextBlock afflictionName = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), name, font: GUI.SubHeadingFont); + GUITextBlock afflictionName = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), name, font: GUIStyle.SubHeadingFont); - GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUI.LargeFont) + GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; @@ -702,7 +702,7 @@ namespace Barotrauma } }; - EnsureTextDoesntOverflow(name, afflictionName, textContainer.Rect, ImmutableArray.Create(textLayout, parentLayout)); + EnsureTextDoesntOverflow(name.Value, afflictionName, textContainer.Rect, ImmutableArray.Create(textLayout, parentLayout)); healElement.Afflictions.Add(new PendingAfflictionElement(affliction, backgroundFrame, healCost)); @@ -720,8 +720,8 @@ namespace Barotrauma GUILayoutGroup textGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), parent.RectTransform)); - string? characterName = info.Name, - jobName = null; + string? characterName = info.Name; + LocalizedString? jobName = null; GUITextBlock? nameBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), textGroup.RectTransform), characterName), jobBlock = null; @@ -741,7 +741,7 @@ namespace Barotrauma if (jobBlock is null) { return; } - EnsureTextDoesntOverflow(jobName, jobBlock, parent.Rect, layoutGroups); + EnsureTextDoesntOverflow(jobName?.Value, jobBlock, parent.Rect, layoutGroups); } } @@ -766,14 +766,14 @@ namespace Barotrauma mainFrame.RectTransform.ScreenSpaceOffset = new Point((int)location.X, GameMain.GraphicsHeight - mainFrame.Rect.Height); } - GUITextBlock feedbackBlock = new GUITextBlock(new RectTransform(Vector2.One, mainFrame.RectTransform), TextManager.Get("pleasewaitupnp"), textAlignment: Alignment.Center, font: GUI.LargeFont, wrap: true) + GUITextBlock feedbackBlock = new GUITextBlock(new RectTransform(Vector2.One, mainFrame.RectTransform), TextManager.Get("pleasewaitupnp"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont, wrap: true) { Visible = true }; GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), mainLayout.RectTransform), TextManager.Get("medicalclinic.treatall")) { - Font = GUI.SubHeadingFont, + Font = GUIStyle.SubHeadingFont, Visible = false }; @@ -793,7 +793,7 @@ namespace Barotrauma if (request.Result != MedicalClinic.RequestResult.Success) { feedbackBlock.Text = GetErrorText(request.Result); - feedbackBlock.TextColor = GUI.Style.Red; + feedbackBlock.TextColor = GUIStyle.Red; return; } @@ -844,11 +844,11 @@ namespace Barotrauma GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); - GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUI.SubHeadingFont); + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); - Color textColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, (int)affliction.AfflictionSeverity / 2f); + Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, (int)affliction.AfflictionSeverity / 2f); - string vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); + LocalizedString vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center) { TextColor = textColor, @@ -857,8 +857,8 @@ namespace Barotrauma AutoScaleHorizontal = true }; - string severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); - GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + LocalizedString severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); + GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { TextColor = textColor, DisabledTextColor = textColor * 0.5f, @@ -866,17 +866,17 @@ namespace Barotrauma AutoScaleHorizontal = true }; - EnsureTextDoesntOverflow(prefab.Name, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + EnsureTextDoesntOverflow(prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUILayoutGroup bottomTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), bottomLayout.RectTransform)); - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), ToolBox.LimitString(prefab.Description, GUI.IntScale(64)), wrap: true) + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), ToolBox.LimitString(prefab.Description, GUIStyle.Font, GUI.IntScale(64)), wrap: true) { ToolTip = prefab.Description }; - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUI.LargeFont); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); @@ -968,7 +968,7 @@ namespace Barotrauma if (GameMain.IsSingleplayer || !(pendingHealList is { ErrorBlock: { } errorBlock, HealList: { } healList })) { return; } errorBlock.Visible = true; - errorBlock.TextColor = GUI.Style.TextColor; + errorBlock.TextColor = GUIStyle.TextColorNormal; errorBlock.Text = TextManager.Get("pleasewaitupnp"); healList.Visible = false; @@ -983,7 +983,7 @@ namespace Barotrauma if (request.Result != MedicalClinic.RequestResult.Success) { errorBlock.Text = GetErrorText(request.Result); - errorBlock.TextColor = GUI.Style.Red; + errorBlock.TextColor = GUIStyle.Red; return; } @@ -1011,7 +1011,7 @@ namespace Barotrauma selectedCrewAfflictionList = null; } - private static string GetErrorText(MedicalClinic.RequestResult result) + private static LocalizedString GetErrorText(MedicalClinic.RequestResult result) { return result switch { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 4d2e47df8..47df0dea8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -10,6 +10,25 @@ namespace Barotrauma { class Store { + class ItemQuantity + { + public int Total { get; private set; } + public int NonEmpty { get; private set; } + public bool AllNonEmpty => NonEmpty == Total; + + public ItemQuantity(int total, bool areNonEmpty = true) + { + Total = total; + NonEmpty = areNonEmpty ? total : 0; + } + + public void Add(int amount, bool areNonEmpty) + { + Total += amount; + if (areNonEmpty) { NonEmpty += amount; } + } + } + private readonly CampaignUI campaignUI; private readonly GUIComponent parentComponent; private readonly List storeTabButtons = new List(); @@ -44,7 +63,7 @@ namespace Barotrauma private Point resolutionWhenCreated; - private Dictionary OwnedItems { get; } = new Dictionary(); + private Dictionary OwnedItems { get; } = new Dictionary(); private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; @@ -302,10 +321,10 @@ namespace Barotrauma }; var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreTradingIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUIStyle.LargeFont) { CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; // Merchant balance ------------------------------------------------ @@ -319,13 +338,13 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), - TextManager.Get("campaignstore.storebalance"), font: GUI.Font, textAlignment: Alignment.BottomLeft) + TextManager.Get("campaignstore.storebalance"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; merchantBalanceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), - "", font: GUI.SubHeadingFont) + "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, TextScale = 1.1f, @@ -343,11 +362,11 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), - TextManager.Get("campaignstore.sellvalue"), font: GUI.Font, textAlignment: Alignment.BottomLeft) + TextManager.Get("campaignstore.sellvalue"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; var valueChangeGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) @@ -356,9 +375,9 @@ namespace Barotrauma RelativeSpacing = 0.02f }; float blockWidth = GUI.IsFourByThree() ? 0.32f : 0.28f; - Point blockMaxSize = new Point((int)(GameSettings.TextScale * 60), valueChangeGroup.Rect.Height); + Point blockMaxSize = new Point((int)(GameSettings.CurrentConfig.Graphics.TextScale * 60), valueChangeGroup.Rect.Height); currentSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, - "", font: GUI.SubHeadingFont) + "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, CanBeFocused = false, @@ -416,7 +435,7 @@ namespace Barotrauma Visible = false }; newSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, - "", font: GUI.SubHeadingFont) + "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, CanBeFocused = false, @@ -435,7 +454,7 @@ namespace Barotrauma tabSortingMethods.Clear(); foreach (StoreTab tab in tabs) { - string text = tab switch + LocalizedString text = tab switch { StoreTab.SellSub => TextManager.Get("submarine"), _ => TextManager.Get("campaignstoretab." + tab) @@ -591,10 +610,10 @@ namespace Barotrauma }; imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreShoppingCrateIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaignstore.shoppingcrate"), font: GUI.LargeFont, textAlignment: Alignment.Right) + new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaignstore.shoppingcrate"), font: GUIStyle.LargeFont, textAlignment: Alignment.Right) { CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; // Player balance ------------------------------------------------ @@ -603,13 +622,13 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - TextManager.Get("campaignstore.balance"), font: GUI.Font, textAlignment: Alignment.BottomRight) + TextManager.Get("campaignstore.balance"), font: GUIStyle.Font, textAlignment: Alignment.BottomRight) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + "", textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopRight) { AutoScaleVertical = true, TextScale = 1.1f, @@ -638,11 +657,11 @@ namespace Barotrauma { Stretch = true }; - relevantBalanceName = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", font: GUI.Font) + relevantBalanceName = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", font: GUIStyle.Font) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { CanBeFocused = false, TextScale = 1.1f, @@ -653,11 +672,11 @@ namespace Barotrauma { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUI.Font) + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUIStyle.Font) { CanBeFocused = false }; - shoppingCrateTotal = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + shoppingCrateTotal = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { CanBeFocused = false, TextScale = 1.1f @@ -666,14 +685,14 @@ namespace Barotrauma var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight); confirmButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform)) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; SetConfirmButtonBehavior(); clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { ClickSound = GUISoundType.DecreaseQuantity, Enabled = HasActiveTabPermissions(), - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, OnClicked = (button, userData) => { if (!HasActiveTabPermissions()) { return false; } @@ -694,9 +713,9 @@ namespace Barotrauma resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } - private string GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); + private LocalizedString GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); - private string GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); + private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { @@ -709,7 +728,7 @@ namespace Barotrauma var iconWidth = (0.9f * dealsHeader.Rect.Height) / dealsHeader.Rect.Width; var dealsIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 0.9f), dealsHeader.RectTransform), "StoreDealIcon", scaleToFit: true); var text = TextManager.Get(parentList == storeBuyList ? "campaignstore.dailyspecials" : "campaignstore.requestedgoods"); - var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUI.LargeFont); + var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUIStyle.LargeFont); storeSpecialColor = dealsIcon.Color; dealsText.TextColor = storeSpecialColor; var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine"); @@ -811,7 +830,7 @@ namespace Barotrauma child.Visible = (IsBuying || item.Quantity > 0) && (!category.HasValue || item.ItemPrefab.Category.HasFlag(category.Value)) && - (string.IsNullOrEmpty(filter) || item.ItemPrefab.Name.ToLower().Contains(filter)); + (string.IsNullOrEmpty(filter) || item.ItemPrefab.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)); } foreach (GUIButton btn in itemCategoryButtons) { @@ -892,7 +911,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = quantity; SetQuantityLabelText(StoreTab.Buy, itemFrame); - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetPriceGetters(itemFrame, true); } SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); @@ -967,7 +986,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; SetQuantityLabelText(StoreTab.Sell, itemFrame); - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetPriceGetters(itemFrame, false); } SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); @@ -1045,7 +1064,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; SetQuantityLabelText(StoreTab.SellSub, itemFrame); - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetPriceGetters(itemFrame, false); } SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); @@ -1185,7 +1204,7 @@ namespace Barotrauma numInput.Enabled = hasPermissions; numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab); } - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetItemFrameStatus(itemFrame, hasPermissions); } existingItemFrames.Add(itemFrame); @@ -1193,7 +1212,7 @@ namespace Barotrauma suppressBuySell = true; if (numInput != null) { - if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUI.Style.Green); } + if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); } numInput.IntValue = item.Quantity; } suppressBuySell = false; @@ -1421,14 +1440,8 @@ namespace Barotrauma width = parentComponent.Rect.Width; parent = parentComponent.RectTransform; } - string tooltip = pi.ItemPrefab.Name; - if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) - { - tooltip += $"\n{pi.ItemPrefab.Description}"; - } GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, (int)(GUI.yScale * 80)), parent: parent), style: "ListBoxElement") { - ToolTip = tooltip, UserData = pi }; @@ -1443,7 +1456,7 @@ namespace Barotrauma var iconRelativeWidth = 0.0f; var priceAndButtonRelativeWidth = 1.0f - nameAndIconRelativeWidth; - if ((pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite) is { } itemIcon) + if ((pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.Sprite) is { } itemIcon) { iconRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; GUIImage img = new GUIImage(new RectTransform(new Vector2(iconRelativeWidth, 0.9f), mainGroup.RectTransform), itemIcon, scaleToFit: true) @@ -1468,7 +1481,7 @@ namespace Barotrauma bool locationHasDealOnItem = isSellingRelatedList ? CurrentLocation.RequestedGoods.Contains(pi.ItemPrefab) : CurrentLocation.DailySpecials.Contains(pi.ItemPrefab); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), - pi.ItemPrefab.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft) + pi.ItemPrefab.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, @@ -1498,7 +1511,7 @@ namespace Barotrauma if (isParentOnLeftSideOfInterface) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform), - CreateQuantityLabelText(containingTab, pi.Quantity), font: GUI.Font, textAlignment: Alignment.BottomLeft) + CreateQuantityLabelText(containingTab, pi.Quantity), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, @@ -1545,8 +1558,7 @@ namespace Barotrauma var rectTransform = shoppingCrateAmountGroup == null ? new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform) : new RectTransform(new Vector2(0.6f, 1.0f), shoppingCrateAmountGroup.RectTransform); - new GUITextBlock(rectTransform, CreateOwnedLabelText(OwnedItems.GetValueOrDefault(pi.ItemPrefab, 0)), font: GUI.Font, - textAlignment: shoppingCrateAmountGroup == null ? Alignment.TopLeft : Alignment.CenterLeft) + var ownedLabel = new GUITextBlock(rectTransform, string.Empty, font: GUIStyle.Font, textAlignment: shoppingCrateAmountGroup == null ? Alignment.TopLeft : Alignment.CenterLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, @@ -1554,6 +1566,7 @@ namespace Barotrauma TextScale = 0.85f, UserData = "owned" }; + SetOwnedText(frame, ownedLabel); shoppingCrateAmountGroup?.Recalculate(); var buttonRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; @@ -1563,7 +1576,7 @@ namespace Barotrauma CanBeFocused = false }; var priceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), priceFrame.RectTransform, anchor: Anchor.Center), - "0 MK", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + "0 MK", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { CanBeFocused = false, TextColor = locationHasDealOnItem ? storeSpecialColor : Color.White, @@ -1577,7 +1590,7 @@ namespace Barotrauma new RectTransform(new Vector2(1.0f, 0.25f), priceFrame.RectTransform, anchor: Anchor.Center) { AbsoluteOffset = new Point(0, priceBlock.RectTransform.ScaledSize.Y) - }, "", font: GUI.SmallFont, textAlignment: Alignment.Center) + }, "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { CanBeFocused = false, Strikethrough = new GUITextBlock.StrikethroughSettings(color: priceBlock.TextColor, expand: 1), @@ -1593,7 +1606,7 @@ namespace Barotrauma { ClickSound = GUISoundType.IncreaseQuantity, Enabled = !forceDisable && pi.Quantity > 0, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = "addbutton", OnClicked = (button, userData) => AddToShoppingCrate(pi) }; @@ -1604,7 +1617,7 @@ namespace Barotrauma { ClickSound = GUISoundType.DecreaseQuantity, Enabled = !forceDisable, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = "removebutton", OnClicked = (button, userData) => ClearFromShoppingCrate(pi) }; @@ -1639,7 +1652,7 @@ namespace Barotrauma if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; } if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; } if (!ItemAndAllContainersInteractable(subItem)) { continue; } - AddToOwnedItems(subItem.Prefab); + AddOwnedItem(subItem); } } @@ -1650,11 +1663,11 @@ namespace Barotrauma var rootInventoryOwner = item.GetRootInventoryOwner(); var ownedByCrewMember = GameMain.GameSession.CrewManager.GetCharacters().Any(c => c == rootInventoryOwner); if (!ownedByCrewMember) { continue; } - AddToOwnedItems(item.Prefab); + AddOwnedItem(item); } // Add items already purchased - CargoManager?.PurchasedItems?.ForEach(pi => AddToOwnedItems(pi.ItemPrefab, amount: pi.Quantity)); + CargoManager?.PurchasedItems?.ForEach(pi => AddNonEmptyOwnedItems(pi)); ownedItemsUpdateTimer = 0.0f; @@ -1668,15 +1681,30 @@ namespace Barotrauma return true; } - void AddToOwnedItems(ItemPrefab itemPrefab, int amount = 1) + void AddOwnedItem(Item item) { - if (OwnedItems.ContainsKey(itemPrefab)) + if (!(item?.Prefab.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo)) { return; } + bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f; + if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity)) { - OwnedItems[itemPrefab] += amount; + OwnedItems[item.Prefab].Add(1, isNonEmpty); } else { - OwnedItems.Add(itemPrefab, amount); + OwnedItems.Add(item.Prefab, new ItemQuantity(1, areNonEmpty: isNonEmpty)); + } + } + + void AddNonEmptyOwnedItems(PurchasedItem purchasedItem) + { + if (purchasedItem == null) { return; } + if (OwnedItems.TryGetValue(purchasedItem.ItemPrefab, out ItemQuantity itemQuantity)) + { + OwnedItems[purchasedItem.ItemPrefab].Add(purchasedItem.Quantity, true); + } + else + { + OwnedItems.Add(purchasedItem.ItemPrefab, new ItemQuantity(purchasedItem.Quantity)); } } } @@ -1692,7 +1720,7 @@ namespace Barotrauma { icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f: 0.5f); } - else if (pi.ItemPrefab?.sprite != null) + else if (pi.ItemPrefab?.Sprite != null) { icon.Color = pi.ItemPrefab.SpriteColor * (enabled ? 1.0f : 0.5f); } @@ -1737,35 +1765,89 @@ namespace Barotrauma itemFrame.UserData = pi; } - private void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame) + private static void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame) { - if (itemFrame == null) { return; } - if (itemFrame.FindChild("quantitylabel", recursive: true) is GUITextBlock label) + if (itemFrame?.FindChild("quantitylabel", recursive: true) is GUITextBlock label) { label.Text = CreateQuantityLabelText(mode, (itemFrame.UserData as PurchasedItem).Quantity); } } - private string CreateQuantityLabelText(StoreTab mode, int quantity) => mode != StoreTab.Buy ? - TextManager.GetWithVariable("campaignstore.quantity", "[amount]", quantity.ToString()) : - TextManager.GetWithVariable("campaignstore.instock", "[amount]", quantity.ToString()); - - private void SetOwnedLabelText(GUIComponent itemComponent) + private static LocalizedString CreateQuantityLabelText(StoreTab mode, int quantity) { - if (itemComponent == null) { return; } - var itemCount = 0; - if (itemComponent.UserData is PurchasedItem pi) + try { - itemCount = OwnedItems.GetValueOrDefault(pi.ItemPrefab, itemCount); + string textTag = mode switch + { + StoreTab.Buy => "campaignstore.instock", + StoreTab.Sell => "campaignstore.ownedinventory", + StoreTab.SellSub => "campaignstore.ownedsub", + _ => throw new NotImplementedException() + }; + return TextManager.GetWithVariable(textTag, "[amount]", quantity.ToString()); } - if (itemComponent.FindChild("owned", recursive: true) is GUITextBlock label) + catch (NotImplementedException e) { - label.Text = CreateOwnedLabelText(itemCount); + string errorMsg = $"Error creating a store quantity label text: unknown store tab.\n{e.StackTrace.CleanupStackTrace()}"; +#if DEBUG + DebugConsole.ShowError(errorMsg); +#else + DebugConsole.AddWarning(errorMsg); +#endif } + return string.Empty; } - private string CreateOwnedLabelText(int itemCount) => itemCount > 0 ? - TextManager.GetWithVariable("campaignstore.owned", "[amount]", itemCount.ToString()) : ""; + private void SetOwnedText(GUIComponent itemComponent, GUITextBlock ownedLabel = null) + { + ownedLabel ??= itemComponent?.FindChild("owned", recursive: true) as GUITextBlock; + if (itemComponent == null && ownedLabel == null) { return; } + PurchasedItem purchasedItem = itemComponent?.UserData as PurchasedItem; + ItemQuantity itemQuantity = null; + LocalizedString ownedLabelText = string.Empty; + if (purchasedItem != null && OwnedItems.TryGetValue(purchasedItem.ItemPrefab, out itemQuantity) && itemQuantity.Total > 0) + { + if (itemQuantity.AllNonEmpty) + { + ownedLabelText = TextManager.GetWithVariable("campaignstore.owned", "[amount]", itemQuantity.Total.ToString()); + } + else + { + ownedLabelText = TextManager.GetWithVariables("campaignstore.ownedspecific", + ("[nonempty]", itemQuantity.NonEmpty.ToString()), + ("[total]", itemQuantity.Total.ToString())); + } + } + if (itemComponent != null) + { + LocalizedString toolTip = string.Empty; + if (purchasedItem.ItemPrefab != null) + { + toolTip = purchasedItem.ItemPrefab.Name; + if (!purchasedItem.ItemPrefab.Description.IsNullOrEmpty()) + { + toolTip += $"\n{purchasedItem.ItemPrefab.Description}"; + } + if (itemQuantity != null) + { + if (itemQuantity.AllNonEmpty) + { + toolTip += $"\n\n{ownedLabelText}"; + } + else + { + toolTip += $"\n\n{TextManager.GetWithVariable("campaignstore.ownednonempty", "[amount]", itemQuantity.NonEmpty.ToString())}"; + toolTip += $"\n{TextManager.GetWithVariable("campaignstore.ownedtotal", "[amount]", itemQuantity.Total.ToString())}"; + } + } + } + itemComponent.ToolTip = toolTip; + } + if (ownedLabel != null) + { + ownedLabel.Text = ownedLabelText; + } + } private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode) { @@ -1799,7 +1881,7 @@ namespace Barotrauma } } - private string GetCurrencyFormatted(int amount) => + private LocalizedString GetCurrencyFormatted(int amount) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); private bool ModifyBuyQuantity(PurchasedItem item, int quantity) @@ -1916,7 +1998,7 @@ namespace Barotrauma var dialog = new GUIMessageBox( TextManager.Get("newsupplies"), TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), - new string[] { TextManager.Get("Ok") }); + new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; return false; @@ -1995,7 +2077,7 @@ namespace Barotrauma var confirmDialog = new GUIMessageBox( TextManager.Get("FireWarningHeader"), TextManager.Get("CampaignStore.SellWarningText"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); confirmDialog.Buttons[0].OnClicked = (b, o) => SellItems(); confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; @@ -2040,12 +2122,12 @@ namespace Barotrauma ownedItemsUpdateTimer += deltaTime; if (ownedItemsUpdateTimer >= timerUpdateInterval) { - var prevOwnedItems = new Dictionary(OwnedItems); + var prevOwnedItems = new Dictionary(OwnedItems); UpdateOwnedItems(); var refresh = (prevOwnedItems.Count != OwnedItems.Count) || - (prevOwnedItems.Select(kvp => kvp.Value).Sum() != OwnedItems.Select(kvp => kvp.Value).Sum()) || - (OwnedItems.Any(kvp => kvp.Value > 0 && !prevOwnedItems.ContainsKey(kvp.Key)) || - prevOwnedItems.Any(kvp => !OwnedItems.TryGetValue(kvp.Key, out var itemCount) || kvp.Value != itemCount)); + (prevOwnedItems.Select(kvp => kvp.Value.Total).Sum() != OwnedItems.Select(kvp => kvp.Value.Total).Sum()) || + (OwnedItems.Any(kvp => kvp.Value.Total > 0 && !prevOwnedItems.ContainsKey(kvp.Key)) || + prevOwnedItems.Any(kvp => !OwnedItems.TryGetValue(kvp.Key, out ItemQuantity itemQuantity) || kvp.Value.Total != itemQuantity.Total)); if (refresh) { needsItemsToSellRefresh = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 1c687913c..2d8d5a598 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -31,19 +31,12 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private string purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; - public static readonly string[] DeliveryTextVariables = new string[] { "[submarinename1]", "[location1]", "[location2]", "[submarinename2]", "[amount]", "[currencyname]" }; - public static readonly string[] SwitchTextVariables = new string[] { "[submarinename1]", "[submarinename2]" }; - public static readonly string[] PurchaseAndSwitchTextVariables = new string[] { "[submarinename1]", "[amount]", "[currencyname]", "[submarinename2]" }; - public static readonly string[] PurchaseTextVariables = new string[] { "[submarinename]", "[amount]", "[currencyname]" }; - - private static readonly string[] notEnoughCreditsDeliveryTextVariables = new string[] { "[currencyname]", "[submarinename]", "[location1]", "[location2]" }; - private static readonly string[] notEnoughCreditsPurchaseTextVariables = new string[] { "[currencyname]", "[submarinename]" }; - private readonly string[] messageBoxOptions; + private readonly LocalizedString[] messageBoxOptions; public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; @@ -77,11 +70,11 @@ namespace Barotrauma if (GameMain.Client == null) { - messageBoxOptions = new string[2] { TextManager.Get("Yes"), TextManager.Get("Cancel") }; + messageBoxOptions = new LocalizedString[2] { TextManager.Get("Yes"), TextManager.Get("Cancel") }; } else { - messageBoxOptions = new string[2] { TextManager.Get("Yes") + " " + TextManager.Get("initiatevoting"), TextManager.Get("Cancel") }; + messageBoxOptions = new LocalizedString[2] { TextManager.Get("Yes") + " " + TextManager.Get("initiatevoting"), TextManager.Get("Cancel") }; } if (Submarine.MainSub?.Info == null) { return; } @@ -107,7 +100,7 @@ namespace Barotrauma } currencyShorthandText = TextManager.Get("currencyformat"); - currencyLongText = TextManager.Get("credit").ToLower(); + currencyLongText = TextManager.Get("credit").Value.ToLowerInvariant(); UpdateSubmarines(); missingPreviewText = TextManager.Get("SubPreviewImageNotFound"); @@ -135,9 +128,9 @@ namespace Barotrauma }; content = new GUILayoutGroup(new RectTransform(new Point(background.Rect.Width - HUDLayoutSettings.Padding * 4, background.Rect.Height - HUDLayoutSettings.Padding * 4), background.RectTransform, Anchor.Center)) { AbsoluteSpacing = (int)(HUDLayoutSettings.Padding * 1.5f) }; - GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.Name), font: GUI.LargeFont); + GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.Name), font: GUIStyle.LargeFont); header.CalculateHeightFromText(0, true); - GUITextBlock credits = new GUITextBlock(new RectTransform(Vector2.One, header.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight) + GUITextBlock credits = new GUITextBlock(new RectTransform(Vector2.One, header.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { TextGetter = CampaignUI.GetMoney }; @@ -159,7 +152,7 @@ namespace Barotrauma specsFrame = new GUIListBox(new RectTransform(new Vector2(0.39f, 1f), infoFrame.RectTransform), style: null) { Spacing = GUI.IntScale(5), Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding, 0, 0) }; new GUIFrame(new RectTransform(new Vector2(0.02f, 0.8f), infoFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, style: "VerticalLine"); GUIListBox descriptionFrame = new GUIListBox(new RectTransform(new Vector2(0.59f, 1f), infoFrame.RectTransform), style: null) { Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding / 2f) }; - descriptionTextBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionFrame.Content.RectTransform), string.Empty, font: GUI.Font, wrap: true) { CanBeFocused = false }; + descriptionTextBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionFrame.Content.RectTransform), string.Empty, font: GUIStyle.Font, wrap: true) { CanBeFocused = false }; GUILayoutGroup buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), content.RectTransform), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; @@ -180,7 +173,7 @@ namespace Barotrauma SetConfirmButtonState(false); pageIndicatorHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.5f), submarineControlsGroup.RectTransform), style: null); - pageIndicator = GUI.Style.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); + pageIndicator = GUIStyle.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); UpdatePaging(); for (int i = 0; i < submarineDisplays.Length; i++) @@ -191,9 +184,9 @@ namespace Barotrauma }; submarineDisplayElement.submarineImage = new GUIImage(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), null, true); submarineDisplayElement.middleTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), string.Empty, textAlignment: Alignment.Center); - submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont); - submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUI.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Center); - submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont); + submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Center); + submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); submarineDisplayElement.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null); submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton") { @@ -342,7 +335,7 @@ namespace Barotrauma if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { - string amountString = currencyShorthandText.Replace("[credits]", subToDisplay.Price.ToString()); + LocalizedString amountString = currencyShorthandText.Replace("[credits]", subToDisplay.Price.ToString()); submarineDisplays[i].submarineFee.Text = priceText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -351,7 +344,7 @@ namespace Barotrauma { if (deliveryFee > 0) { - string amountString = currencyShorthandText.Replace("[credits]", deliveryFee.ToString()); + LocalizedString amountString = currencyShorthandText.Replace("[credits]", deliveryFee.ToString()); submarineDisplays[i].submarineFee.Text = deliveryFeeText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -535,7 +528,7 @@ namespace Barotrauma listBackground.Sprite = previewImage; listBackground.SetCrop(true); - ScalableFont font = GUI.Font; + GUIFont font = GUIStyle.Font; info.CreateSpecsWindow(specsFrame, font); descriptionTextBlock.Text = info.Description; descriptionTextBlock.CalculateHeightFromText(); @@ -590,8 +583,11 @@ namespace Barotrauma { if (GameMain.GameSession.Campaign.Money < deliveryFee && deliveryFee > 0) { - new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", notEnoughCreditsDeliveryTextVariables, - new string[] { currencyLongText, selectedSubmarine.DisplayName, deliveryLocationName, GameMain.GameSession.Map.CurrentLocation.Name })); + new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", + ("[currencyname]", currencyLongText), + ("[submarinename]", selectedSubmarine.DisplayName), + ("[location1]", deliveryLocationName), + ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); return; } @@ -599,13 +595,19 @@ namespace Barotrauma if (deliveryFee > 0) { - msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", DeliveryTextVariables, - new string[6] { selectedSubmarine.DisplayName, deliveryLocationName, GameMain.GameSession.Map.CurrentLocation.Name, CurrentOrPendingSubmarine().DisplayName, deliveryFee.ToString(), currencyLongText }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", + ("[submarinename1]", selectedSubmarine.DisplayName), + ("[location1]", deliveryLocationName), + ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), + ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), + ("[amount]", deliveryFee.ToString()), + ("[currencyname]", currencyLongText)), messageBoxOptions); } else { - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", SwitchTextVariables, - new string[2] { CurrentOrPendingSubmarine().DisplayName, selectedSubmarine.DisplayName }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", + ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), + ("[submarinename2]", selectedSubmarine.DisplayName)), messageBoxOptions); } msgBox.Buttons[0].OnClicked = (applyButton, obj) => @@ -629,8 +631,9 @@ namespace Barotrauma { if (GameMain.GameSession.Campaign.Money < selectedSubmarine.Price) { - new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", notEnoughCreditsPurchaseTextVariables, - new string[2] { currencyLongText, selectedSubmarine.DisplayName })); + new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", + ("[currencyname]", currencyLongText), + ("[submarinename]", selectedSubmarine.DisplayName))); return; } @@ -638,8 +641,11 @@ namespace Barotrauma if (!purchaseOnly) { - msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", PurchaseAndSwitchTextVariables, - new string[4] { selectedSubmarine.DisplayName, selectedSubmarine.Price.ToString(), currencyLongText, CurrentOrPendingSubmarine().DisplayName }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", + ("[submarinename1]", selectedSubmarine.DisplayName), + ("[amount]", selectedSubmarine.Price.ToString()), + ("[currencyname]", currencyLongText), + ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { @@ -658,8 +664,10 @@ namespace Barotrauma } else { - msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", PurchaseTextVariables, - new string[3] { selectedSubmarine.DisplayName, selectedSubmarine.Price.ToString(), currencyLongText }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", + ("[submarinename]", selectedSubmarine.DisplayName), + ("[amount]", selectedSubmarine.Price.ToString()), + ("[currencyname]", currencyLongText)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 7be59a84f..aa22ddcd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -99,15 +99,15 @@ namespace Barotrauma { if (currentPing < lowPingThreshold) { - return GUI.Style.Green; + return GUIStyle.Green; } else if (currentPing < mediumPingThreshold) { - return GUI.Style.Yellow; + return GUIStyle.Yellow; } else { - return GUI.Style.Red; + return GUIStyle.Red; } } @@ -119,10 +119,10 @@ namespace Barotrauma public void Initialize() { - spectateIcon = GUI.Style.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0]; - disconnectedIcon = GUI.Style.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0]; - ownerIcon = GUI.Style.GetComponentStyle("OwnerIcon").GetDefaultSprite(); - moderatorIcon = GUI.Style.GetComponentStyle("ModeratorIcon").GetDefaultSprite(); + spectateIcon = GUIStyle.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0]; + disconnectedIcon = GUIStyle.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0]; + ownerIcon = GUIStyle.GetComponentStyle("OwnerIcon").GetDefaultSprite(); + moderatorIcon = GUIStyle.GetComponentStyle("ModeratorIcon").GetDefaultSprite(); initialized = true; } @@ -142,7 +142,7 @@ namespace Barotrauma talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) { - talentApplyButton.Flash(GUI.Style.Orange); + talentApplyButton.Flash(GUIStyle.Orange); } } @@ -243,7 +243,7 @@ namespace Barotrauma var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation"); var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame"); - new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), "", textAlignment: Alignment.Right, parseRichText: true) + new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), "", textAlignment: Alignment.Right) { TextGetter = () => TextManager.GetWithVariable("campaignmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", campaignMode.Money)) }; @@ -353,7 +353,7 @@ namespace Barotrauma { if (teamIDs.Count > 1) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUI.Style.Green : GUI.Style.Orange) { ForceUpperCase = true }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUIStyle.Green : GUIStyle.Orange) { ForceUpperCase = ForceUpperCase.Yes }; } headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true) @@ -396,7 +396,7 @@ namespace Barotrauma for (int i = 0; i < teamIDs.Count; i++) { - headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUI.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f); + headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUIStyle.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f); if (!GameMain.IsMultiplayer) { @@ -446,9 +446,9 @@ namespace Barotrauma jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUIStyle.HotkeyFont; jobButton.CanBeFocused = characterButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = true; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -493,7 +493,7 @@ namespace Barotrauma }; GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, null)); } @@ -510,9 +510,9 @@ namespace Barotrauma characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont; jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = true; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -583,11 +583,11 @@ namespace Barotrauma else { GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); if (character is AICharacter) { - linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = true })); + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes })); } else { @@ -677,16 +677,16 @@ namespace Barotrauma float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth; characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUI.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); + ToolBox.LimitString(client.Name, GUIStyle.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); float iconWidth = iconSize.X / (float)characterColumnWidth; - int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUI.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); + int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; } else { characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: nameColor); + ToolBox.LimitString(client.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: nameColor); } if (client.Character != null && client.Character.IsDead) @@ -724,14 +724,14 @@ namespace Barotrauma } else { - Vector2 stringOffset = GUI.GlobalFont.MeasureString(inLobbyString) / 2f; - GUI.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); + Vector2 stringOffset = GUIStyle.GlobalFont.MeasureString(inLobbyString) / 2f; + GUIStyle.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); } } private void DrawDisconnectedIcon(SpriteBatch spriteBatch, Rectangle area) { - disconnectedIcon.Draw(spriteBatch, area, GUI.Style.Red); + disconnectedIcon.Draw(spriteBatch, area, GUIStyle.Red); } /// @@ -788,7 +788,7 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client)); - ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; + GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font; var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { @@ -796,9 +796,9 @@ namespace Barotrauma Stretch = true }; - GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUI.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUI.Font) + GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUIStyle.Font) { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero }; @@ -885,22 +885,22 @@ namespace Barotrauma switch (type) { case PlayerConnectionChangeType.Joined: - textColor = GUI.Style.Green; + textColor = GUIStyle.Green; break; case PlayerConnectionChangeType.Kicked: - textColor = GUI.Style.Orange; + textColor = GUIStyle.Orange; break; case PlayerConnectionChangeType.Disconnected: - textColor = GUI.Style.Yellow; + textColor = GUIStyle.Yellow; break; case PlayerConnectionChangeType.Banned: - textColor = GUI.Style.Red; + textColor = GUIStyle.Red; break; } if (logList != null) { - var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont, parseRichText: true) + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), RichString.Rich(line), wrap: true, font: GUIStyle.SmallFont) { TextColor = textColor, CanBeFocused = false, @@ -935,14 +935,14 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(10) }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("Biome", fallBackTag: "location"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)Level.Loaded.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") @@ -974,7 +974,7 @@ namespace Barotrauma if (GameMain.GameSession?.Missions != null) { int spacing = GUI.IntScale(5); - int iconSize = (int)(GUI.LargeFont.MeasureChar('T').Y + GUI.Font.MeasureChar('T').Y * 4 + spacing * 4); + int iconSize = (int)(GUIStyle.LargeFont.MeasureChar('T').Y + GUIStyle.Font.MeasureChar('T').Y * 4 + spacing * 4); foreach (Mission mission in GameMain.GameSession.Missions) { @@ -983,28 +983,27 @@ namespace Barotrauma { AbsoluteSpacing = spacing }; - string descriptionText = mission.Description; - foreach (string missionMessage in mission.ShownMessages) + LocalizedString descriptionText = mission.Description; + foreach (LocalizedString missionMessage in mission.ShownMessages) { descriptionText += "\n\n" + missionMessage; } - string rewardText = mission.GetMissionRewardText(Submarine.MainSub); - string reputationText = mission.GetReputationRewardText(mission.Locations[0]); + RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); + RichString reputationText = mission.GetReputationRewardText(mission.Locations[0]); - var missionNameRichTextData = RichTextData.GetRichTextData(mission.Name, out string missionNameString); - var missionRewardRichTextData = RichTextData.GetRichTextData(rewardText, out string missionRewardString); - var missionReputationRichTextData = RichTextData.GetRichTextData(reputationText, out string missionReputationString); - var missionDescriptionRichTextData = RichTextData.GetRichTextData(descriptionText, out string missionDescriptionString); + Func wrapMissionText(GUIFont font) + { + return (str) => ToolBox.WrapText(str, missionTextGroup.Rect.Width, font.Value); + } + RichString missionNameString = RichString.Rich(mission.Name, wrapMissionText(GUIStyle.LargeFont)); + RichString missionRewardString = RichString.Rich(rewardText, wrapMissionText(GUIStyle.Font)); + RichString missionReputationString = RichString.Rich(reputationText, wrapMissionText(GUIStyle.Font)); + RichString missionDescriptionString = RichString.Rich(descriptionText, wrapMissionText(GUIStyle.Font)); - missionNameString = ToolBox.WrapText(missionNameString, missionTextGroup.Rect.Width, GUI.LargeFont); - missionRewardString = ToolBox.WrapText(missionRewardString, missionTextGroup.Rect.Width, GUI.Font); - missionReputationString = ToolBox.WrapText(missionReputationString, missionTextGroup.Rect.Width, GUI.Font); - missionDescriptionString = ToolBox.WrapText(missionDescriptionString, missionTextGroup.Rect.Width, GUI.Font); - - Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); - Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); - Vector2 missionReputationSize = GUI.Font.MeasureString(missionReputationString); + Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); + Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString); + Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString); float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; bool displayDifficulty = mission.Difficulty.HasValue; @@ -1030,7 +1029,7 @@ namespace Barotrauma UpdateMissionStateIcon(mission, icon); mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon(mission, icon); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameRichTextData, missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); GUILayoutGroup difficultyIndicatorGroup = null; if (displayDifficulty) { @@ -1048,20 +1047,20 @@ namespace Barotrauma }; } } - var rewardTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardRichTextData, missionRewardString); + var rewardTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); if (difficultyIndicatorGroup != null) { difficultyIndicatorGroup.RectTransform.Resize(new Point((int)(difficultyIndicatorGroup.Rect.Width - rewardTextBlock.Padding.X - rewardTextBlock.Padding.Z), difficultyIndicatorGroup.Rect.Height)); difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)rewardTextBlock.Padding.X, 0); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionReputationRichTextData, missionReputationString); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionRichTextData, missionDescriptionString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionReputationString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } } else { GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUIStyle.LargeFont); } } @@ -1101,11 +1100,11 @@ namespace Barotrauma GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, style: null); GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.319f, 0f) }, false, childAnchor: Anchor.TopLeft); - string missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUI.LargeFont); - string missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUI.Font); + LocalizedString missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUIStyle.LargeFont); + LocalizedString missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUIStyle.Font); - Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y)); missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); @@ -1118,7 +1117,7 @@ namespace Barotrauma new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), traitorMission.Icon, null, true) { Color = traitorMission.IconColor }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } @@ -1164,21 +1163,21 @@ namespace Barotrauma var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform)); - string className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}") : TextManager.Get("shuttle"); + LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}") : TextManager.Get("shuttle"); - int nameHeight = (int)GUI.LargeFont.MeasureString(sub.Info.DisplayName, true).Y; - int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; + int nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y; + int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; - var submarineNameText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, nameHeight + HUDLayoutSettings.Padding / 2), subInfoTextLayout.RectTransform), sub.Info.DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; + var submarineNameText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, nameHeight + HUDLayoutSettings.Padding / 2), subInfoTextLayout.RectTransform), sub.Info.DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false }; submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); - var submarineClassText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, classHeight), subInfoTextLayout.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; + var submarineClassText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, classHeight), subInfoTextLayout.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); if (GameMain.GameSession?.GameMode is CampaignMode campaign) { GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.09f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0f, 0.43f) }, isHorizontal: true) { Stretch = true }; GUIImage headerIcon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SubmarineIcon"); - new GUITextBlock(new RectTransform(Vector2.One, headerLayout.RectTransform), TextManager.Get("uicategory.upgrades"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(Vector2.One, headerLayout.RectTransform), TextManager.Get("uicategory.upgrades"), font: GUIStyle.LargeFont); var upgradeRootLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.48f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), isHorizontal: true); @@ -1206,7 +1205,7 @@ namespace Barotrauma else { var specsListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.57f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft)); - sub.Info.CreateSpecsWindow(specsListBox, GUI.Font, includeTitle: false, includeClass: false, includeDescription: true); + sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); } } private Color unselectedColor = new Color(240, 255, 255, 225); @@ -1216,8 +1215,8 @@ namespace Barotrauma private Color pressedColor = new Color(60, 60, 60, 225); private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>(); - private readonly List<(string talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(string talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>(); - private List selectedTalents = new List(); + private readonly List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>(); + private List selectedTalents = new List(); private GUITextBlock experienceText; private GUIProgressBar experienceBar; @@ -1231,11 +1230,11 @@ namespace Barotrauma private readonly ImmutableDictionary talentStageStyles = new Dictionary { - { TalentTree.TalentTreeStageState.Invalid, GUI.Style.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Locked, GUI.Style.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Unlocked, GUI.Style.GetComponentStyle("TalentTreePurchased") }, - { TalentTree.TalentTreeStageState.Available, GUI.Style.GetComponentStyle("TalentTreeUnlocked") }, - { TalentTree.TalentTreeStageState.Highlighted, GUI.Style.GetComponentStyle("TalentTreeAvailable") }, + { TalentTree.TalentTreeStageState.Invalid, GUIStyle.GetComponentStyle("TalentTreeLocked") }, + { TalentTree.TalentTreeStageState.Locked, GUIStyle.GetComponentStyle("TalentTreeLocked") }, + { TalentTree.TalentTreeStageState.Unlocked, GUIStyle.GetComponentStyle("TalentTreePurchased") }, + { TalentTree.TalentTreeStageState.Available, GUIStyle.GetComponentStyle("TalentTreeUnlocked") }, + { TalentTree.TalentTreeStageState.Highlighted, GUIStyle.GetComponentStyle("TalentTreeAvailable") }, }.ToImmutableDictionary(); private readonly ImmutableDictionary talentStageBackgroundColors = new Dictionary @@ -1287,7 +1286,7 @@ namespace Barotrauma { AbsoluteSpacing = GUI.IntScale(5) }; - + GUILayoutGroup talentInfoLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), talentFrameLayoutGroup.RectTransform, Anchor.Center), isHorizontal: true); CharacterInfo info = controlledCharacter.Info; @@ -1300,18 +1299,18 @@ namespace Barotrauma }); GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), talentInfoLayoutGroup.RectTransform)) { RelativeSpacing = 0.05f }; - - Vector2 nameSize = GUI.SubHeadingFont.MeasureString(info.Name); - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUI.SubHeadingFont) { TextColor = job.Prefab.UIColor }; + + Vector2 nameSize = GUIStyle.SubHeadingFont.MeasureString(info.Name); + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont) { TextColor = job.Prefab.UIColor }; nameBlock.RectTransform.NonScaledSize = nameSize.Pad(nameBlock.Padding).ToPoint(); - Vector2 jobSize = GUI.SmallFont.MeasureString(job.Name); - GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUI.SmallFont) { TextColor = job.Prefab.UIColor }; + Vector2 jobSize = GUIStyle.SmallFont.MeasureString(job.Name); + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; jobBlock.RectTransform.NonScaledSize = jobSize.Pad(jobBlock.Padding).ToPoint(); - string traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); - Vector2 traitSize = GUI.SmallFont.MeasureString(traitString); - GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUI.SmallFont); + LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); + Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); + GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); GUIFrame endocrineFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), nameLayout.RectTransform, Anchor.BottomCenter), style: null); @@ -1365,7 +1364,7 @@ namespace Barotrauma } } - IEnumerable endocrineTalents = info.GetEndocrineTalents().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(e, StringComparison.OrdinalIgnoreCase))); + IEnumerable endocrineTalents = info.GetEndocrineTalents().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); if (endocrineTalents.Count() > 0) { @@ -1377,15 +1376,15 @@ namespace Barotrauma GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), talentInfoLayoutGroup.RectTransform)) { Stretch = true }; - string skillString = TextManager.Get("skills"); - Vector2 skillSize = GUI.SubHeadingFont.MeasureString(skillString); - GUITextBlock skillBlock = new GUITextBlock(new RectTransform(Vector2.One, skillLayout.RectTransform), skillString, font: GUI.SubHeadingFont); + LocalizedString skillString = TextManager.Get("skills"); + Vector2 skillSize = GUIStyle.SubHeadingFont.MeasureString(skillString); + GUITextBlock skillBlock = new GUITextBlock(new RectTransform(Vector2.One, skillLayout.RectTransform), skillString, font: GUIStyle.SubHeadingFont); skillBlock.RectTransform.NonScaledSize = skillSize.Pad(skillBlock.Padding).ToPoint(); skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); CreateTalentSkillList(controlledCharacter, skillListBox); - if (!TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + if (!TalentTree.JobTalentTrees.TryGet(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } new GUIFrame(new RectTransform(new Vector2(1f, 1f), talentFrameLayoutGroup.RectTransform), style: "HorizontalLine"); @@ -1401,7 +1400,7 @@ namespace Barotrauma int elementPadding = GUI.IntScale(8); Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); - subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUI.SubHeadingFont, textAlignment: Alignment.Center)); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); for (int i = 0; i < 4; i++) { @@ -1439,7 +1438,7 @@ namespace Barotrauma GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) { - ToolTip = $"{talent.DisplayName}\n\n{talent.Description}", + ToolTip = RichString.Rich(talent.DisplayName + "\n\n" + talent.Description), UserData = talent.Identifier, PressedColor = pressedColor, OnClicked = (button, userData) => @@ -1447,7 +1446,7 @@ namespace Barotrauma // deselect other buttons in tier by removing their selected talents from pool foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) { - if (guiButton.UserData is string otherTalentIdentifier && guiButton != button) + if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button) { if (!controlledCharacter.HasTalent(otherTalentIdentifier)) { @@ -1455,7 +1454,7 @@ namespace Barotrauma } } } - string talentIdentifier = userData as string; + Identifier talentIdentifier = (Identifier)userData; if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents)) { @@ -1479,10 +1478,10 @@ namespace Barotrauma GUIComponent iconImage; if (talent.Icon is null) { - iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) + iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null) { - OutlineColor = GUI.Style.Red, - TextColor = GUI.Style.Red, + OutlineColor = GUIStyle.Red, + TextColor = GUIStyle.Red, PressedColor = unselectableColor, CanBeFocused = false, }; @@ -1511,18 +1510,18 @@ namespace Barotrauma GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), - barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: GUI.Style.Green) + barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) { IsHorizontal = true, }; - - experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUI.Font, textAlignment: Alignment.CenterRight) + + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) { Shadow = true, ToolTip = TextManager.Get("experiencetooltip") }; - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUI.SubHeadingFont, parseRichText: true, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") { @@ -1545,22 +1544,23 @@ namespace Barotrauma { GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = false }; - skillNames.Add(new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}", returnNull: true) ?? skill.Identifier)); + skillNames.Add(new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value))); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.CenterRight) { Padding = new Vector4(0, 0, 4, 0) }; float modifiedSkillLevel = character.GetSkillLevel(skill.Identifier); if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); + //TODO: if/when we upgrade to C# 9, do neater pattern matching here string stringColor = true switch { - true when skillChange > 0 => XMLExtensions.ColorToString(GUI.Style.Green), - true when skillChange < 0 => XMLExtensions.ColorToString(GUI.Style.Red), - _ => XMLExtensions.ColorToString(GUI.Style.TextColor) + true when skillChange > 0 => XMLExtensions.ColorToString(GUIStyle.Green), + true when skillChange < 0 => XMLExtensions.ColorToString(GUIStyle.Red), + _ => XMLExtensions.ColorToString(GUIStyle.TextColorNormal) }; - string changeText = $"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"; - new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText, parseRichText: true) { Padding = Vector4.Zero }; + RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"); + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero }; } skillContainer.Recalculate(); } @@ -1601,8 +1601,8 @@ namespace Barotrauma } else if (talentCount > 0) { - string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUI.Style.Red)}‖{-talentCount}‖color:end‖"; - string localizedString = TextManager.GetWithVariables("talentmenu.points.spending", new []{ "[amount]", "[used]" }, new []{ pointsLeft, pointsUsed}); + string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUIStyle.Red)}‖{-talentCount}‖color:end‖"; + LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed)); talentPointText.SetRichText(localizedString); } else @@ -1622,19 +1622,19 @@ namespace Barotrauma foreach (var talentButton in talentButtons) { - string talentIdentifier = talentButton.button.UserData as string; + Identifier talentIdentifier = (Identifier)talentButton.button.UserData; bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier); Color newTalentColor = unselectable ? unselectableColor : unselectedColor; Color hoverColor = Color.White; if (controlledCharacter.HasTalent(talentIdentifier)) { - newTalentColor = GUI.Style.Green; + newTalentColor = GUIStyle.Green; } else if (selectedTalents.Contains(talentIdentifier)) { - newTalentColor = GUI.Style.Orange; - hoverColor = Color.Lerp(GUI.Style.Orange, Color.White, 0.7f); + newTalentColor = GUIStyle.Orange; + hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f); } talentButton.icon.Color = newTalentColor; @@ -1647,7 +1647,7 @@ namespace Barotrauma private void ApplyTalents(Character controlledCharacter) { selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); - foreach (string talent in selectedTalents) + foreach (Identifier talent in selectedTalents) { controlledCharacter.GiveTalent(talent); if (GameMain.Client != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index 4d79d57fd..54c5ee076 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -19,11 +19,7 @@ namespace Barotrauma private set; } - public bool Slice - { - get; - set; - } + public bool Slice => Slices != null; public Rectangle[] Slices { @@ -54,7 +50,7 @@ namespace Barotrauma public TransitionMode TransitionMode { get; private set; } - public UISprite(XElement element) + public UISprite(ContentXElement element) { Sprite = new Sprite(element); MaintainAspectRatio = element.GetAttributeBool("maintainaspectratio", false); @@ -69,6 +65,7 @@ namespace Barotrauma } Vector4 sliceVec = element.GetAttributeVector4("slice", Vector4.Zero); + Slices = null; if (sliceVec != Vector4.Zero) { minBorderScale = element.GetAttributeFloat("minborderscale", 0.1f); @@ -76,7 +73,6 @@ namespace Barotrauma Rectangle slice = new Rectangle((int)sliceVec.X, (int)sliceVec.Y, (int)(sliceVec.Z - sliceVec.X), (int)(sliceVec.W - sliceVec.Y)); - Slice = true; Slices = new Rectangle[9]; //top-left diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 258c55b37..a5be0901b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -245,7 +245,7 @@ namespace Barotrauma * |----------------------------| */ GUILayoutGroup tooltipLayout = new GUILayoutGroup(rectT(0.95f,0.95f, ItemInfoFrame, Anchor.Center)) { Stretch = true }; - new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty, font: GUI.SubHeadingFont) { UserData = "itemname" }; + new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty, font: GUIStyle.SubHeadingFont) { UserData = "itemname" }; new GUITextBlock(rectT(1, 0, tooltipLayout), TextManager.Get("UpgradeUITooltip.UpgradeListHeader")); new GUIListBox(rectT(1, 0.5f, tooltipLayout), style: null) { ScrollBarVisible = false, AutoHideScrollBar = false, SmoothScroll = true, UserData = "upgradelist"}; new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; @@ -268,7 +268,7 @@ namespace Barotrauma GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; GUILayoutGroup locationLayout = new GUILayoutGroup(rectT(1, 0.5f, leftLayout), isHorizontal: true); GUIImage submarineIcon = new GUIImage(rectT(new Point(locationLayout.Rect.Height, locationLayout.Rect.Height), locationLayout), style: "SubmarineIcon", scaleToFit: true); - new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUI.LargeFont); + new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; @@ -285,9 +285,9 @@ namespace Barotrauma */ GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; - new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUI.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(AvailableMoney, format: true), font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(AvailableMoney, format: true) }; - new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; + new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); + new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(AvailableMoney, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(AvailableMoney, format: true) }; + new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => { @@ -351,7 +351,7 @@ namespace Barotrauma if (schematicsSprite == null) { return; } float schematicsScale = Math.Min(component.Rect.Width / 2 / schematicsSprite.size.X, component.Rect.Height / schematicsSprite.size.Y); Vector2 center = new Vector2(component.Rect.Center.X, component.Rect.Center.Y); - schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUI.Style.Green, new Vector2(0, schematicsSprite.size.Y / 2), + schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUIStyle.Green, new Vector2(0, schematicsSprite.size.Y / 2), scale: schematicsScale); var swappableItemList = selectedUpgradeCategoryLayout?.FindChild("prefablist", true) as GUIListBox; @@ -359,10 +359,10 @@ namespace Barotrauma ItemPrefab swapTo = highlightedElement?.UserData as ItemPrefab ?? selectedItem.PendingItemSwap; if (swapTo?.SwappableItem == null) { return; } Sprite? schematicsSprite2 = swapTo.SwappableItem?.SchematicSprite; - schematicsSprite2?.Draw(spriteBatch, new Vector2(component.Rect.Right, center.Y), GUI.Style.Orange, new Vector2(schematicsSprite2.size.X, schematicsSprite2.size.Y / 2), + schematicsSprite2?.Draw(spriteBatch, new Vector2(component.Rect.Right, center.Y), GUIStyle.Orange, new Vector2(schematicsSprite2.size.X, schematicsSprite2.size.Y / 2), scale: Math.Min(component.Rect.Width / 2 / schematicsSprite2.size.X, component.Rect.Height / schematicsSprite2.size.Y)); - var arrowSprite = GUI.Style?.GetComponentStyle("GUIButtonToggleRight")?.GetDefaultSprite(); + var arrowSprite = GUIStyle.GetComponentStyle("GUIButtonToggleRight")?.GetDefaultSprite(); if (arrowSprite != null) { arrowSprite.Draw(spriteBatch, center, scale: GUI.Scale); @@ -428,7 +428,7 @@ namespace Barotrauma if (AvailableMoney >= hullRepairCost) { - string body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); + LocalizedString body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { if (AvailableMoney >= hullRepairCost) @@ -463,7 +463,7 @@ namespace Barotrauma { if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) { - string body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); + LocalizedString body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) @@ -493,7 +493,7 @@ namespace Barotrauma { foreach (var (item, itemFrame) in itemPreviews) { - itemFrame.OutlineColor = itemFrame.Color = isHovered && item.GetComponent() == null ? GUI.Style.Orange : previewWhite; + itemFrame.OutlineColor = itemFrame.Color = isHovered && item.GetComponent() == null ? GUIStyle.Orange : previewWhite; } return true; }); @@ -509,7 +509,7 @@ namespace Barotrauma if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { - string body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); + LocalizedString body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) @@ -540,7 +540,7 @@ namespace Barotrauma { if (subInfo.LeftBehindDockingPortIDs.Contains(item.ID)) { - itemFrame.OutlineColor = itemFrame.Color = subInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUI.Style.Red : GUI.Style.Green; + itemFrame.OutlineColor = itemFrame.Color = subInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUIStyle.Red : GUIStyle.Green; } else { @@ -551,7 +551,7 @@ namespace Barotrauma }, disableElement: true); } - private void CreateRepairEntry(GUIComponent parent, string title, string imageStyle, int price, GUIButton.OnClickedHandler onPressed, bool isDisabled, Func? onHover = null, bool disableElement = false) + private void CreateRepairEntry(GUIComponent parent, LocalizedString title, string imageStyle, int price, GUIButton.OnClickedHandler onPressed, bool isDisabled, Func? onHover = null, bool disableElement = false) { GUIFrame frameChild = new GUIFrame(rectT(new Point(parent.Rect.Width, (int) (96 * GUI.Scale)), parent), style: "UpgradeUIFrame"); frameChild.SelectedColor = frameChild.Color; @@ -569,7 +569,7 @@ namespace Barotrauma GUILayoutGroup contentLayout = new GUILayoutGroup(rectT(0.9f, 0.85f, frameChild, Anchor.Center), isHorizontal: true); var repairIcon = new GUIFrame(rectT(new Point(contentLayout.Rect.Height, contentLayout.Rect.Height), contentLayout), style: imageStyle); GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true }; - new GUITextBlock(rectT(1, 0, textLayout), title, font: GUI.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; + new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = AvailableMoney >= price && !isDisabled, OnClicked = onPressed }; @@ -661,7 +661,7 @@ namespace Barotrauma * |-----------------------------|--------------------------| */ GUILayoutGroup contentLayout = new GUILayoutGroup(rectT(0.9f, 0.85f, frameChild, Anchor.Center)); - var itemCategoryLabel = new GUITextBlock(rectT(1, 1, contentLayout), category.Name, font: GUI.SubHeadingFont) { CanBeFocused = false }; + var itemCategoryLabel = new GUITextBlock(rectT(1, 1, contentLayout), category.Name, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; GUILayoutGroup indicatorLayout = new GUILayoutGroup(rectT(0.5f, 0.25f, contentLayout, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.TopRight) { UserData = "indicators", IgnoreLayoutGroups = true, RelativeSpacing = 0.01f }; foreach (var prefab in prefabs) @@ -742,7 +742,7 @@ namespace Barotrauma GUIComponent[] categoryFrames = GetFrames(category); foreach (GUIComponent itemFrame in itemPreviews.Values) { - itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUIStyle.Orange : previewWhite; itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } @@ -790,7 +790,7 @@ namespace Barotrauma GUIComponent[] categoryFrames = GetFrames(category); foreach (GUIComponent itemFrame in itemPreviews.Values) { - itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUIStyle.Orange : previewWhite; itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } return true; @@ -851,8 +851,8 @@ namespace Barotrauma if (linkedItems.Min(it => it.ID) < item.ID) { return; } var currentOrPending = item.PendingItemSwap ?? item.Prefab; - string name = currentOrPending.Name; - string nameWithQuantity = ""; + LocalizedString name = currentOrPending.Name; + LocalizedString nameWithQuantity = ""; if (linkedItems.Count > 1) { foreach (ItemPrefab distinctItem in linkedItems.Select(it => it.Prefab).Distinct()) @@ -881,7 +881,7 @@ namespace Barotrauma }; GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1f, 1f, toggleButton.Frame), isHorizontal: true); - string slotText = ""; + LocalizedString slotText = ""; if (linkedItems.Count > 1) { slotText = TextManager.GetWithVariable("weaponslot", "[number]", string.Join(", ", linkedItems.Select(it => (swappableEntities.IndexOf(it) + 1).ToString()))); @@ -891,13 +891,13 @@ namespace Barotrauma slotText = TextManager.GetWithVariable("weaponslot", "[number]", (swappableEntities.IndexOf(item) + 1).ToString()); } - new GUITextBlock(rectT(0.3f, 1f, buttonLayout), text: slotText, font: GUI.SubHeadingFont); + new GUITextBlock(rectT(0.3f, 1f, buttonLayout), text: slotText, font: GUIStyle.SubHeadingFont); GUILayoutGroup group = new GUILayoutGroup(rectT(0.7f, 1f, buttonLayout), isHorizontal: true) { Stretch = true }; - string title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : nameWithQuantity; - GUITextBlock text = new GUITextBlock(rectT(0.7f, 1f, group), text: title, font: GUI.SubHeadingFont, textAlignment: Alignment.Right, parseRichText: true) + var title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : nameWithQuantity; + GUITextBlock text = new GUITextBlock(rectT(0.7f, 1f, group), text: RichString.Rich(title), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { - TextColor = GUI.Style.Orange + TextColor = GUIStyle.Orange }; GUIImage arrowImage = new GUIImage(rectT(0.5f, 1f, group, scaleBasis: ScaleBasis.BothHeight), style: "SlideDownArrow", scaleToFit: true); @@ -911,7 +911,7 @@ namespace Barotrauma List frames = new List(); if (currentOrPending != null) { - bool canUninstall = item.PendingItemSwap != null || !string.IsNullOrEmpty(currentOrPending.SwappableItem?.ReplacementOnUninstall); + bool canUninstall = item.PendingItemSwap != null || !(currentOrPending.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true); bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; if (isUninstallPending) { canUninstall = false; } @@ -928,9 +928,7 @@ namespace Barotrauma { string textTag = item.PendingItemSwap != null ? "upgrades.cancelitemswappromptbody" : "upgrades.itemuninstallpromptbody"; if (isUninstallPending) { textTag = "upgrades.cancelitemuninstallpromptbody"; } - string promptBody = TextManager.GetWithVariables(textTag, - new[] { "[itemtouninstall]" }, - new[] { isUninstallPending ? item.Name : currentOrPending.Name }); + LocalizedString promptBody = TextManager.GetWithVariable(textTag, "[itemtouninstall]", isUninstallPending ? item.Name : currentOrPending.Name); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("upgrades.refundprompttitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -970,9 +968,9 @@ namespace Barotrauma buyButton.Enabled = true; buyButton.OnClicked += (button, o) => { - string promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", - new[] { "[itemtoinstall]", "[amount]" }, - new[] { replacement.Name, (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count).ToString() }); + LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", + ("[itemtoinstall]", replacement.Name), + ("[amount]", (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1019,7 +1017,7 @@ namespace Barotrauma var linkedItems = Campaign.UpgradeManager.GetLinkedItemsToSwap(item); foreach (var itemPreview in itemPreviews) { - itemPreview.Value.OutlineColor = itemPreview.Value.Color = linkedItems.Contains(itemPreview.Key) ? GUI.Style.Orange : previewWhite; + itemPreview.Value.OutlineColor = itemPreview.Value.Color = linkedItems.Contains(itemPreview.Key) ? GUIStyle.Orange : previewWhite; } foreach (GUIComponent otherComponent in toggleButton.Parent.Children) { @@ -1041,7 +1039,7 @@ namespace Barotrauma foreach (var itemPreview in itemPreviews) { if (currentStoreLayout?.SelectedData is CategoryData categoryData && !categoryData.Category.ItemTags.Any(t => itemPreview.Key.HasTag(t))) { continue; } - itemPreview.Value.OutlineColor = itemPreview.Value.Color = GUI.Style.Orange; + itemPreview.Value.OutlineColor = itemPreview.Value.Color = GUIStyle.Orange; } } activeItemSwapSlideDown = toggleButton.Selected ? toggleButton : null; @@ -1058,7 +1056,7 @@ namespace Barotrauma return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } - public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, string title, string body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab upgradePrefab = null, int currentLevel = 0) + public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0) { float progressBarHeight = 0.25f; @@ -1080,29 +1078,29 @@ namespace Barotrauma GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), title, font: GUI.SubHeadingFont, parseRichText: true) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); - var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; GUILayoutGroup? progressLayout = null; GUILayoutGroup? buyButtonLayout = null; if (addProgressBar) { progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; - new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUI.Style.Orange); - new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUI.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); + new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; } if (addBuyButton) { - string formattedPrice = FormatCurrency(Math.Abs(price)); + var formattedPrice = FormatCurrency(Math.Abs(price)); //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center); if (price < 0) { - priceText.TextColor = GUI.Style.Green; + priceText.TextColor = GUIStyle.Green; } else if (price == 0) { @@ -1120,8 +1118,8 @@ namespace Barotrauma // cut the description if it overflows and add a tooltip to it for (int i = 100; i > 0 && description.Rect.Height > descriptionLayout.Rect.Height; i--) { - string[] lines = description.WrappedText.Split('\n'); - var newString = string.Join('\n', lines.Take(lines.Length - 1)); + var lines = description.WrappedText.Split('\n'); + var newString = string.Join('\n', lines.Take(lines.Count - 1)); if (0 >= newString.Length - 4) { break; } description.Text = newString.Substring(0, newString.Length - 4) + "..."; @@ -1185,7 +1183,9 @@ namespace Barotrauma buyButton.OnClicked += (button, o) => { - string promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", new []{ "[upgradename]", "[amount]"}, new []{ prefab.Name, prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString() }); + LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", + ("[upgradename]", prefab.Name), + ("[amount]", prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1225,7 +1225,7 @@ namespace Barotrauma itemName.Text = entity is Item ? entity.Name : TextManager.Get("upgradecategory.walls"); if (slotIndex > -1) { - itemName.Text = TextManager.GetWithVariables("weaponslotwithname", new string[] { "[number]", "[weaponname]" }, new string[] { slotIndex.ToString(), itemName.Text }); + itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text)); } upgradeList.Content.ClearChildren(); for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++) @@ -1246,7 +1246,7 @@ namespace Barotrauma { if (textBlock.UserData is Tuple tuple && tuple.Item2 == prefab) { - string tooltip = CreateListEntry(tuple.Item2.Name, level + tuple.Item1); + var tooltip = CreateListEntry(tuple.Item2.Name, level + tuple.Item1); textBlock.Text = tooltip; found = true; break; @@ -1275,7 +1275,7 @@ namespace Barotrauma moreIndicator.CalculateHeightFromText(); layout.Recalculate(); - static string CreateListEntry(string name, int level) => TextManager.GetWithVariables("upgradeuitooltip.upgradelistelement", new[] { "[upgradename]", "[level]" }, new[] { name, $"{level}" }); + static LocalizedString CreateListEntry(LocalizedString name, int level) => TextManager.GetWithVariables("upgradeuitooltip.upgradelistelement", ("[upgradename]", name), ("[level]", $"{level}")); } public static IEnumerable GetApplicableCategories(Submarine drawnSubmarine) @@ -1343,7 +1343,7 @@ namespace Barotrauma if (selectedUpgradeCategoryLayout != null) { var linkedItems = HoveredItem is Item ? Campaign.UpgradeManager.GetLinkedItemsToSwap((Item)HoveredItem) : new List(); - if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem || linkedItems.Contains(c.UserData as Item), recursive: true) is GUIButton itemElement) + if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem || linkedItems.Contains((Item)c.UserData), recursive: true) is GUIButton itemElement) { if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); @@ -1417,9 +1417,9 @@ namespace Barotrauma */ submarineInfoFrame = new GUILayoutGroup(rectT(0.25f, 0.2f, mainStoreLayout, Anchor.TopRight)) { IgnoreLayoutGroups = true }; // submarine name - new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUI.LargeFont); + new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUIStyle.LargeFont); // submarine class - new GUITextBlock(rectT(1, 0, submarineInfoFrame), $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}", textAlignment: Alignment.Right, font: GUI.Font); + new GUITextBlock(rectT(1, 0, submarineInfoFrame), $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}", textAlignment: Alignment.Right, font: GUIStyle.Font); var description = new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.Description, textAlignment: Alignment.Right, wrap: true); submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale)); @@ -1448,7 +1448,7 @@ namespace Barotrauma Point size = new Point((int) (spriteSize * item.Scale / dockedBorders.Width * hullContainer.Rect.Width)); itemFrame = new GUIImage(rectT(size, component, Anchor.Center), icon, scaleToFit: true) { - SelectedColor = GUI.Style.Orange, + SelectedColor = GUIStyle.Orange, Color = previewWhite, HoverCursor = CursorState.Hand, SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None @@ -1457,7 +1457,7 @@ namespace Barotrauma { new GUIImage(new RectTransform(new Vector2(0.8f), itemFrame.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(-0.2f) }, "WeaponSwitchIcon.DropShadow", scaleToFit: true) { - SelectedColor = GUI.Style.Orange, + SelectedColor = GUIStyle.Orange, Color = previewWhite, CanBeFocused = false }; @@ -1468,7 +1468,7 @@ namespace Barotrauma Point size = new Point((int) (item.Rect.Width * item.Scale / dockedBorders.Width * hullContainer.Rect.Width), (int) (item.Rect.Height * item.Scale / dockedBorders.Height * hullContainer.Rect.Height)); itemFrame = new GUIFrame(rectT(size, component, Anchor.Center), style: "ScanLines") { - SelectedColor = GUI.Style.Orange, + SelectedColor = GUIStyle.Orange, OutlineColor = previewWhite, Color = previewWhite, OutlineThickness = 2, @@ -1540,7 +1540,7 @@ namespace Barotrauma // calculate the center point so we can draw a line from X to Y instead of drawing a rotated rectangle that is filled Vector2 point1 = hullVertex[1] + (hullVertex[2] - hullVertex[1]) / 2; Vector2 point2 = hullVertex[0] + (hullVertex[3] - hullVertex[0]) / 2; - GUI.DrawLine(spriteBatch, point1, point2, (highlightWalls ? GUI.Style.Orange * 0.6f : Color.DarkCyan * 0.3f), width: 10); + GUI.DrawLine(spriteBatch, point1, point2, (highlightWalls ? GUIStyle.Orange * 0.6f : Color.DarkCyan * 0.3f), width: 10); if (GameMain.DebugDraw) { // the "collision box" is a bit bigger than the line we draw so this can be useful data (maybe) @@ -1553,14 +1553,14 @@ namespace Barotrauma { int currentLevel = campaign.UpgradeManager.GetUpgradeLevel(prefab, category); - string progressText = TextManager.GetWithVariables("upgrades.progressformat", new[] { "[level]", "[maxlevel]" }, new[] { currentLevel.ToString(), prefab.MaxLevel.ToString() }); + LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", prefab.MaxLevel.ToString())); if (prefabFrame.FindChild("progressbar", true) is { } progressParent) { GUIProgressBar bar = progressParent.GetChild(); if (bar != null) { bar.BarSize = currentLevel / (float) prefab.MaxLevel; - bar.Color = currentLevel >= prefab.MaxLevel ? GUI.Style.Green : GUI.Style.Orange; + bar.Color = currentLevel >= prefab.MaxLevel ? GUIStyle.Green : GUIStyle.Orange; } GUITextBlock block = progressParent.GetChild(); @@ -1620,7 +1620,7 @@ namespace Barotrauma else { parent.Enabled = false; - parent.SelectedColor = GUI.Style.Red * 0.5f; + parent.SelectedColor = GUIStyle.Red * 0.5f; } } @@ -1632,12 +1632,12 @@ namespace Barotrauma { if (component.UserData != prefab) { continue; } - Dictionary styles = GUI.Style.GetComponentStyle("upgradeindicator").ChildStyles; + Dictionary styles = GUIStyle.GetComponentStyle("upgradeindicator").ChildStyles; if (!styles.ContainsKey("upgradeindicatoron") || !styles.ContainsKey("upgradeindicatordim") || !styles.ContainsKey("upgradeindicatoroff")) { continue; } - GUIComponentStyle onStyle = styles["upgradeindicatoron"]; - GUIComponentStyle dimStyle = styles["upgradeindicatordim"]; - GUIComponentStyle offStyle = styles["upgradeindicatoroff"]; + GUIComponentStyle onStyle = styles["upgradeindicatoron".ToIdentifier()]; + GUIComponentStyle dimStyle = styles["upgradeindicatordim".ToIdentifier()]; + GUIComponentStyle offStyle = styles["upgradeindicatoroff".ToIdentifier()]; if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= prefab.MaxLevel) { @@ -1694,7 +1694,7 @@ namespace Barotrauma private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); - public static string FormatCurrency(int money, bool format = true) + public static LocalizedString FormatCurrency(int money, bool format = true) { return TextManager.GetWithVariable("CurrencyFormat", "[credits]", format ? string.Format(CultureInfo.InvariantCulture, "{0:N0}", money) : money.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs index d3feabe8f..5b596f54d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs @@ -34,23 +34,29 @@ namespace Barotrauma public class TextSettings { - public string Text; + public LocalizedString Text; public int Width; + public TextSettings(Identifier textTag, int width) + { + Text = TextManager.GetFormatted(textTag); + Width = width; + } + public TextSettings(XElement element) { - Text = TextManager.GetFormatted(element.GetAttributeString("text", string.Empty), true); + Text = TextManager.GetFormatted(element.GetAttributeIdentifier("text", Identifier.Empty)); Width = element.GetAttributeInt("width", 450); } } public class VideoSettings { - public string File; + public readonly string File; - public VideoSettings(XElement element) + public VideoSettings(string file) { - File = element.GetAttributeString("file", string.Empty); + File = file; } } @@ -75,13 +81,13 @@ namespace Barotrauma } videoView = new GUICustomComponent(new RectTransform(Point.Zero, videoFrame.RectTransform, Anchor.Center), (spriteBatch, guiCustomComponent) => { DrawVideo(spriteBatch, guiCustomComponent.Rect); }); - title = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUI.LargeFont, textColor: new Color(253, 174, 0), textAlignment: Alignment.Left); + title = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUIStyle.LargeFont, textColor: new Color(253, 174, 0), textAlignment: Alignment.Left); - textContent = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUI.Font, textAlignment: Alignment.TopLeft); + textContent = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUIStyle.Font, textAlignment: Alignment.TopLeft); - objectiveTitle = new GUITextBlock(new RectTransform(new Vector2(1f, 0f), textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight, textColor: Color.White); + objectiveTitle = new GUITextBlock(new RectTransform(new Vector2(1f, 0f), textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight, textColor: Color.White); objectiveTitle.Text = TextManager.Get("Tutorial.NewObjective"); - objectiveText = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUI.SubHeadingFont, textColor: new Color(4, 180, 108), textAlignment: Alignment.CenterRight); + objectiveText = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUIStyle.SubHeadingFont, textColor: new Color(4, 180, 108), textAlignment: Alignment.CenterRight); objectiveTitle.Visible = objectiveText.Visible = false; } @@ -120,7 +126,12 @@ namespace Barotrauma background.AddToGUIUpdateList(ignoreChildren, order); } - public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, string contentId, bool startPlayback, string objective = "", Action callback = null) + public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback) + { + LoadContent(contentPath, videoSettings, textSettings, contentId, startPlayback, new RawLString(""), null); + } + + public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback, LocalizedString objective, Action callback = null) { callbackOnStop = callback; filePath = contentPath + videoSettings.File; @@ -183,10 +194,10 @@ namespace Barotrauma title.RectTransform.NonScaledSize = new Point(scaledTextWidth, scaledTitleHeight); title.RectTransform.AbsoluteOffset = new Point((int)(5 * GUI.Scale), (int)(10 * GUI.Scale)); - if (textSettings != null && !string.IsNullOrEmpty(textSettings.Text)) + if (textSettings != null && !textSettings.Text.IsNullOrEmpty()) { - textSettings.Text = ToolBox.WrapText(textSettings.Text, scaledTextWidth, GUI.Font); - int wrappedHeight = textSettings.Text.Split('\n').Length * scaledTextHeight; + textSettings.Text = ToolBox.WrapText(textSettings.Text, scaledTextWidth, GUIStyle.Font); + int wrappedHeight = textSettings.Text.Value.Split('\n').Length * scaledTextHeight; textFrame.RectTransform.NonScaledSize = new Point(scaledTextWidth + scaledBorderSize, wrappedHeight + scaledBorderSize + scaledButtonSize.Y + scaledTitleHeight); @@ -203,7 +214,7 @@ namespace Barotrauma textContent.RectTransform.AbsoluteOffset = new Point(0, scaledBorderSize + scaledTitleHeight); } - if (!string.IsNullOrEmpty(objectiveText.Text)) + if (!objectiveText.Text.IsNullOrEmpty()) { int scaledXOffset = (int)(-10 * GUI.Scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 11489f117..4f5541602 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -19,12 +18,11 @@ namespace Barotrauma private Func getYesVotes, getNoVotes, getMaxVotes; private bool votePassed; - private string votingOnText; - private List votingOnTextData; + private RichString votingOnText; private float votingTime = 100f; private float timer; private VoteType currentVoteType; - private Color submarineColor => GUI.Style.Orange; + private Color submarineColor => GUIStyle.Orange; private Point createdForResolution; public VotingInterface(Client starter, SubmarineInfo info, VoteType type, float votingTime) @@ -60,14 +58,14 @@ namespace Barotrauma int yOffset = padding; int paddedWidth = frame.Rect.Width - padding * 2; - votingTextBlock = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), votingOnTextData, votingOnText, wrap: true); + votingTextBlock = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), votingOnText, wrap: true); votingTextBlock.RectTransform.NonScaledSize = votingTextBlock.RectTransform.MinSize = votingTextBlock.RectTransform.MaxSize = new Point(votingTextBlock.Rect.Width, votingTextBlock.Rect.Height); votingTextBlock.RectTransform.IsFixedSize = true; votingTextBlock.RectTransform.AbsoluteOffset = new Point(padding, yOffset); yOffset += votingTextBlock.Rect.Height + spacing; - voteCounter = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), "(0/0)", GUI.Style.Green, textAlignment: Alignment.Center); + voteCounter = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), "(0/0)", GUIStyle.Green, textAlignment: Alignment.Center); voteCounter.RectTransform.NonScaledSize = voteCounter.RectTransform.MinSize = voteCounter.RectTransform.MaxSize = new Point(voteCounter.Rect.Width, voteCounter.Rect.Height); voteCounter.RectTransform.IsFixedSize = true; voteCounter.RectTransform.AbsoluteOffset = new Point(padding, yOffset); @@ -150,26 +148,41 @@ namespace Barotrauma switch (type) { case VoteType.PurchaseAndSwitchSub: - votingOnText = TextManager.GetWithVariables("submarinepurchaseandswitchvote", new string[] { "[playername]", "[submarinename]", "[amount]", "[currencyname]" }, new string[] { characterRichString, submarineRichString, info.Price.ToString(), TextManager.Get("credit").ToLower() }); + votingOnText = TextManager.GetWithVariables("submarinepurchaseandswitchvote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.PurchaseSub: - votingOnText = TextManager.GetWithVariables("submarinepurchasevote", new string[] { "[playername]", "[submarinename]", "[amount]", "[currencyname]" }, new string[] { characterRichString, submarineRichString, info.Price.ToString(), TextManager.Get("credit").ToLower() }); + votingOnText = TextManager.GetWithVariables("submarinepurchasevote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.SwitchSub: int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); if (deliveryFee > 0) { - votingOnText = TextManager.GetWithVariables("submarineswitchfeevote", new string[] { "[playername]", "[submarinename]", "[locationname]", "[amount]", "[currencyname]" }, new string[] { characterRichString, submarineRichString, endLocation.Name, deliveryFee.ToString(), TextManager.Get("credit").ToLower() }); + votingOnText = TextManager.GetWithVariables("submarineswitchfeevote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString), + ("[locationname]", endLocation.Name), + ("[amount]", deliveryFee.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower())); } else { - votingOnText = TextManager.GetWithVariables("submarineswitchnofeevote", new string[] { "[playername]", "[submarinename]" }, new string[] { characterRichString, submarineRichString }); + votingOnText = TextManager.GetWithVariables("submarineswitchnofeevote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString)); } break; } - votingOnTextData = RichTextData.GetRichTextData(votingOnText, out votingOnText); + votingOnText = RichString.Rich(votingOnText); } private int SubmarineYesVotes() @@ -189,31 +202,50 @@ namespace Barotrauma private void SendSubmarineVoteEndMessage(SubmarineInfo info, VoteType type) { - GameMain.NetworkMember.AddChatMessage(GetSubmarineVoteResultMessage(info, type, yesVotes.ToString(), noVotes.ToString(), votePassed), ChatMessageType.Server); + GameMain.NetworkMember.AddChatMessage(GetSubmarineVoteResultMessage(info, type, yesVotes.ToString(), noVotes.ToString(), votePassed).Value, ChatMessageType.Server); } - public static string GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, string yesVoteString, string noVoteString, bool votePassed) + public static LocalizedString GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, string yesVoteString, string noVoteString, bool votePassed) { - string result = string.Empty; + LocalizedString result = string.Empty; switch (type) { case VoteType.PurchaseAndSwitchSub: - result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", new string[] { "[submarinename]", "[amount]", "[currencyname]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, info.Price.ToString(), TextManager.Get("credit").ToLower(), yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", + ("[submarinename]", info.DisplayName), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower()), + ("[yesvotecount]", yesVoteString), + ("[novotecount]" , noVoteString)); break; case VoteType.PurchaseSub: - result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", new string[] { "[submarinename]", "[amount]", "[currencyname]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, info.Price.ToString(), TextManager.Get("credit").ToLower(), yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", + ("[submarinename]", info.DisplayName), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower()), + ("[yesvotecount]", yesVoteString), + ("[novotecount]", noVoteString)); break; case VoteType.SwitchSub: int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); if (deliveryFee > 0) { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", new string[] { "[submarinename]", "[locationname]", "[amount]", "[currencyname]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, endLocation.Name, deliveryFee.ToString(), TextManager.Get("credit").ToLower(), yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", + ("[submarinename]", info.DisplayName), + ("[locationname]", endLocation.Name), + ("[amount]", deliveryFee.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower()), + ("[yesvotecount]", yesVoteString), + ("[novotecount]", noVoteString)); } else { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", new string[] { "[submarinename]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", + ("[submarinename]", info.DisplayName), + ("[yesvotecount]", yesVoteString), + ("[novotecount]", noVoteString)); } break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs index 2fdbdf0d8..a7a5216d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs @@ -17,7 +17,7 @@ namespace Barotrauma } public Shape shape; - public string tooltip; + public LocalizedString tooltip; public bool showTooltip = true; public Rectangle DrawRect => new Rectangle((int)(DrawPos.X - (float)size / 2), (int)(DrawPos.Y - (float)size / 2), size, size); public Rectangle InputRect @@ -42,7 +42,7 @@ namespace Barotrauma /// public bool isFilled; public int inputAreaMargin; - public Color color = GUI.Style.Red; + public Color color = GUIStyle.Red; public Color? secondaryColor; public Color textColor = Color.White; public Color textBackgroundColor = Color.Black * 0.5f; @@ -183,7 +183,7 @@ namespace Barotrauma } if (IsSelected) { - if (showTooltip && !string.IsNullOrEmpty(tooltip)) + if (showTooltip && !tooltip.IsNullOrEmpty()) { var offset = tooltipOffset ?? new Vector2(size, -size / 2f); GUI.DrawString(spriteBatch, DrawPos + offset, tooltip, textColor, textBackgroundColor); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index 8f0c177f2..e8069b34b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -20,8 +20,8 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(15) }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsentheader"), font: GUI.SubHeadingFont, textColor: Color.White); - var mainText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsenttext"), wrap: true, parseRichText: true); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsentheader"), font: GUIStyle.SubHeadingFont, textColor: Color.White); + var mainText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), RichString.Rich(TextManager.Get("statisticsconsenttext")), wrap: true); foreach (var data in mainText.RichTextData) { @@ -93,7 +93,7 @@ namespace Barotrauma { if (child is GUITextBlock textBlock) { - textBlock.TextScale = MathHelper.Min(1.0f, 1.0f / GameSettings.TextScale); + textBlock.TextScale = MathHelper.Min(1.0f, 1.0f / GameSettings.CurrentConfig.Graphics.TextScale); textBlock.RectTransform.MinSize = new Point(0, (int)textBlock.TextSize.Y); textBlock.RectTransform.MaxSize = new Point(int.MaxValue, (int)textBlock.TextSize.Y); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index f908466c8..c1ed2807f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -45,8 +45,17 @@ namespace Barotrauma public static MainMenuScreen MainMenuScreen; public static NetLobbyScreen NetLobbyScreen; + public static ModDownloadScreen ModDownloadScreen; + + public static void ResetNetLobbyScreen() + { + NetLobbyScreen?.Release(); + NetLobbyScreen = new NetLobbyScreen(); + ModDownloadScreen?.Release(); + ModDownloadScreen = new ModDownloadScreen(); + } + public static ServerListScreen ServerListScreen; - public static SteamWorkshopScreen SteamWorkshopScreen; public static SubEditorScreen SubEditorScreen; public static TestScreen TestScreen; @@ -64,19 +73,7 @@ namespace Barotrauma public static Thread MainThread { get; private set; } - private static ContentPackage vanillaContent; - public static ContentPackage VanillaContent - { - get - { - if (vanillaContent == null) - { - // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.CorePackages.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); - } - return vanillaContent; - } - } + public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage; private static GameSession gameSession; public static GameSession GameSession @@ -94,7 +91,6 @@ namespace Barotrauma } public static ParticleManager ParticleManager; - public static DecalManager DecalManager; private static World world; public static World World @@ -110,10 +106,8 @@ namespace Barotrauma public static LoadingScreen TitleScreen; private bool loadingScreenOpen; - public static GameSettings Config; - private CoroutineHandle loadingCoroutine; - private bool hasLoaded; + public bool HasLoaded { get; private set; } private readonly GameTime fixedTime; @@ -232,9 +226,9 @@ namespace Barotrauma throw new Exception("Content folder not found. If you are trying to compile the game from the source code and own a legal copy of the game, you can copy the Content folder from the game's files to BarotraumaShared/Content."); } - Config = new GameSettings(); - - Md5Hash.LoadCache(); + GameSettings.Init(); + + Md5Hash.Cache.Load(); ConsoleArguments = args; @@ -290,24 +284,42 @@ namespace Barotrauma public void ApplyGraphicsSettings() { - GraphicsWidth = Config.GraphicsWidth; - GraphicsHeight = Config.GraphicsHeight; - switch (Config.WindowMode) + void updateConfig() + { + var config = GameSettings.CurrentConfig; + config.Graphics.Width = GraphicsWidth; + config.Graphics.Height = GraphicsHeight; + GameSettings.SetCurrentConfig(config); + } + + GraphicsWidth = GameSettings.CurrentConfig.Graphics.Width; + GraphicsHeight = GameSettings.CurrentConfig.Graphics.Height; + + if (GraphicsWidth <= 0 || GraphicsHeight <= 0) + { + GraphicsWidth = GraphicsDevice.DisplayMode.Width; + GraphicsHeight = GraphicsDevice.DisplayMode.Height; + updateConfig(); + } + + switch (GameSettings.CurrentConfig.Graphics.DisplayMode) { case WindowMode.BorderlessWindowed: GraphicsWidth = GraphicsDevice.DisplayMode.Width; GraphicsHeight = GraphicsDevice.DisplayMode.Height; + updateConfig(); break; case WindowMode.Windowed: GraphicsWidth = Math.Min(GraphicsDevice.DisplayMode.Width, GraphicsWidth); GraphicsHeight = Math.Min(GraphicsDevice.DisplayMode.Height, GraphicsHeight); + updateConfig(); break; } GraphicsDeviceManager.GraphicsProfile = GfxProfile; GraphicsDeviceManager.PreferredBackBufferFormat = SurfaceFormat.Color; GraphicsDeviceManager.PreferMultiSampling = false; - GraphicsDeviceManager.SynchronizeWithVerticalRetrace = Config.VSyncEnabled; - SetWindowMode(Config.WindowMode); + GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync; + SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode); defaultViewport = GraphicsDevice.Viewport; @@ -317,8 +329,8 @@ namespace Barotrauma public void SetWindowMode(WindowMode windowMode) { WindowMode = windowMode; - GraphicsDeviceManager.HardwareModeSwitch = Config.WindowMode != WindowMode.BorderlessWindowed; - GraphicsDeviceManager.IsFullScreen = Config.WindowMode == WindowMode.Fullscreen || Config.WindowMode == WindowMode.BorderlessWindowed; + GraphicsDeviceManager.HardwareModeSwitch = windowMode != WindowMode.BorderlessWindowed; + GraphicsDeviceManager.IsFullScreen = windowMode == WindowMode.Fullscreen || windowMode == WindowMode.BorderlessWindowed; Window.IsBorderless = !GraphicsDeviceManager.HardwareModeSwitch; GraphicsDeviceManager.PreferredBackBufferWidth = GraphicsWidth; @@ -390,7 +402,7 @@ namespace Barotrauma loadingScreenOpen = true; TitleScreen = new LoadingScreen(GraphicsDevice) { - WaitForLanguageSelection = Config.ShowLanguageSelectionPrompt + WaitForLanguageSelection = GameSettings.CurrentConfig.Language == LanguageIdentifier.None }; bool canLoadInSeparateThread = true; @@ -407,27 +419,31 @@ namespace Barotrauma private IEnumerable Load(bool isSeparateThread) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("LOADING COROUTINE", Color.Lime); } - while (TitleScreen.WaitForLanguageSelection) + ContentPackageManager.LoadVanillaFileList(); + + if (TitleScreen.WaitForLanguageSelection) { - yield return CoroutineStatus.Running; + ContentPackageManager.VanillaCorePackage.LoadFilesOfType(); + TitleScreen.AvailableLanguages = TextManager.AvailableLanguages.ToArray(); + while (TitleScreen.WaitForLanguageSelection) + { + yield return CoroutineStatus.Running; + } + ContentPackageManager.VanillaCorePackage.UnloadFilesOfType(); } SoundManager = new Sounds.SoundManager(); - SoundManager.SetCategoryGainMultiplier("default", Config.SoundVolume, 0); - SoundManager.SetCategoryGainMultiplier("ui", Config.SoundVolume, 0); - SoundManager.SetCategoryGainMultiplier("waterambience", Config.SoundVolume, 0); - SoundManager.SetCategoryGainMultiplier("music", Config.MusicVolume, 0); - SoundManager.SetCategoryGainMultiplier("voip", Math.Min(Config.VoiceChatVolume, 1.0f), 0); + SoundManager.ApplySettings(); - if (Config.EnableSplashScreen && !ConsoleArguments.Contains("-skipintro")) + if (GameSettings.CurrentConfig.EnableSplashScreen && !ConsoleArguments.Contains("-skipintro")) { var pendingSplashScreens = TitleScreen.PendingSplashScreens; - float baseVolume = MathHelper.Clamp(Config.SoundVolume * 2.0f, 0.0f, 1.0f); + float baseVolume = MathHelper.Clamp(GameSettings.CurrentConfig.Audio.SoundVolume * 2.0f, 0.0f, 1.0f); pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_UTG.webm", baseVolume * 0.5f)); pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_FF.webm", baseVolume)); pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_Daedalic.webm", baseVolume * 0.1f)); @@ -443,153 +459,52 @@ namespace Barotrauma } } - GUI.Init(Window, Config.AllEnabledPackages, GraphicsDevice); + GUI.Init(); + + yield return CoroutineStatus.Running; + + var contentPackageLoadRoutine = ContentPackageManager.Init(); + foreach (var progress in contentPackageLoadRoutine) + { + const float min = 1f, max = 70f; + TitleScreen.LoadState = MathHelper.Lerp(min, max, progress.Value); + yield return CoroutineStatus.Running; + } + DebugConsole.Init(); - if (Config.AutoUpdateWorkshopItems) - { - Config.WaitingForAutoUpdate = true; - TaskPool.Add("AutoUpdateWorkshopItemsAsync", - SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => - { - if (!task.TryGetResult(out bool result)) { return; } - - Config.WaitingForAutoUpdate = false; - }); - - while (Config.WaitingForAutoUpdate) { yield return CoroutineStatus.Running; } - } - -#if DEBUG - if (Config.ModBreakerMode) - { - Config.SelectCorePackage(ContentPackage.CorePackages.GetRandom()); - foreach (var regularPackage in ContentPackage.RegularPackages) - { - if (Rand.Range(0.0, 1.0) <= 0.5) - { - Config.EnableRegularPackage(regularPackage); - } - else - { - Config.DisableRegularPackage(regularPackage); - } - } - ContentPackage.SortContentPackages(p => - { - return Rand.Int(int.MaxValue); - }); - } -#endif - - if (Config.AllEnabledPackages.None()) - { - DebugConsole.Log("No content packages selected"); - } - else - { - DebugConsole.Log("Selected content packages: " + string.Join(", ", Config.AllEnabledPackages.Select(cp => cp.Name))); - } - #if !DEBUG && !OSX GameAnalyticsManager.InitIfConsented(); #endif - yield return CoroutineStatus.Running; - - Debug.WriteLine("sounds"); - - int i = 0; - foreach (CoroutineStatus status in SoundPlayer.Init()) - { - if (status == CoroutineStatus.Success) break; - - i++; - TitleScreen.LoadState = SoundPlayer.SoundCount == 0 ? - 1.0f : - Math.Min(40.0f * i / Math.Max(SoundPlayer.SoundCount, 1), 40.0f); - - yield return CoroutineStatus.Running; - } - - TitleScreen.LoadState = 40.0f; - yield return CoroutineStatus.Running; - - LightManager = new Lights.LightManager(base.GraphicsDevice, Content); - - TitleScreen.LoadState = 41.0f; - yield return CoroutineStatus.Running; - - GUI.LoadContent(); - TitleScreen.LoadState = 42.0f; - - yield return CoroutineStatus.Running; - TaskPool.Add("InitRelayNetworkAccess", SteamManager.InitRelayNetworkAccess(), (t) => { }); - FactionPrefab.LoadFactions(); - NPCSet.LoadSets(); - CharacterPrefab.LoadAll(); - MissionPrefab.Init(); - TraitorMissionPrefab.Init(); - MapEntityPrefab.Init(); - Tutorials.Tutorial.Init(); - MapGenerationParams.Init(); - LevelGenerationParams.LoadPresets(); - CaveGenerationParams.LoadPresets(); - OutpostGenerationParams.LoadPresets(); - WreckAIConfig.LoadAll(); - EventSet.LoadPrefabs(); - ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); - AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); - SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); - TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); - TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); - Order.Init(); - EventManagerSettings.Init(); - BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); HintManager.Init(); - TitleScreen.LoadState = 50.0f; yield return CoroutineStatus.Running; - - StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); - TitleScreen.LoadState = 55.0f; - yield return CoroutineStatus.Running; - - UpgradePrefab.LoadAll(GetFilesOfType(ContentType.UpgradeModules)); - TitleScreen.LoadState = 56.0f; - yield return CoroutineStatus.Running; - - JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); - CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); - - NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); - - ItemAssemblyPrefab.LoadAll(); - TitleScreen.LoadState = 60.0f; - yield return CoroutineStatus.Running; - + CoreEntityPrefab.InitCorePrefabs(); GameModePreset.Init(); SaveUtil.DeleteDownloadedSubs(); SubmarineInfo.RefreshSavedSubs(); - TitleScreen.LoadState = 65.0f; + TitleScreen.LoadState = 75.0f; yield return CoroutineStatus.Running; GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice, Content); - TitleScreen.LoadState = 68.0f; + ParticleManager = new ParticleManager(GameScreen.Cam); + LightManager = new Lights.LightManager(base.GraphicsDevice, Content); + + TitleScreen.LoadState = 80.0f; yield return CoroutineStatus.Running; MainMenuScreen = new MainMenuScreen(this); ServerListScreen = new ServerListScreen(); - TitleScreen.LoadState = 70.0f; + TitleScreen.LoadState = 85.0f; yield return CoroutineStatus.Running; #if USE_STEAM - SteamWorkshopScreen = new SteamWorkshopScreen(); if (SteamManager.IsInitialized) { Steamworks.SteamFriends.OnGameRichPresenceJoinRequested += OnInvitedToGame; @@ -599,25 +514,25 @@ namespace Barotrauma { //check the achievements too, so we don't consider people who've played the game before this "gamelaunchcount" stat was added as being 1st-time-players //(people who have played previous versions, but not unlocked any achievements, will be incorrectly considered 1st-time-players, but that should be a small enough group to not skew the statistics) - if (!achievements.Any() && SteamManager.GetStatInt("gamelaunchcount") <= 0) + if (!achievements.Any() && SteamManager.GetStatInt("gamelaunchcount".ToIdentifier()) <= 0) { IsFirstLaunch = true; GameAnalyticsManager.AddDesignEvent("FirstLaunch"); } } - SteamManager.IncrementStat("gamelaunchcount", 1); + SteamManager.IncrementStat("gamelaunchcount".ToIdentifier(), 1); } #endif SubEditorScreen = new SubEditorScreen(); TestScreen = new TestScreen(); - TitleScreen.LoadState = 75.0f; + TitleScreen.LoadState = 90.0f; yield return CoroutineStatus.Running; ParticleEditorScreen = new ParticleEditorScreen(); - TitleScreen.LoadState = 80.0f; + TitleScreen.LoadState = 95.0f; yield return CoroutineStatus.Running; LevelEditorScreen = new LevelEditorScreen(); @@ -628,31 +543,20 @@ namespace Barotrauma yield return CoroutineStatus.Running; - TitleScreen.LoadState = 85.0f; - ParticleManager = new ParticleManager(GameScreen.Cam); - ParticleManager.LoadPrefabs(); - TitleScreen.LoadState = 88.0f; - LevelObjectPrefab.LoadAll(); - - TitleScreen.LoadState = 90.0f; - yield return CoroutineStatus.Running; - - DecalManager = new DecalManager(); - LocationType.Init(); MainMenuScreen.Select(); - foreach (string steamError in SteamManager.InitializationErrors) + foreach (Identifier steamError in SteamManager.InitializationErrors) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get(steamError)); } TitleScreen.LoadState = 100.0f; - hasLoaded = true; - if (GameSettings.VerboseLogging) + HasLoaded = true; + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("LOADING COROUTINE FINISHED", Color.Lime); } - yield return CoroutineStatus.Success; + yield return CoroutineStatus.Success; } @@ -670,23 +574,6 @@ namespace Barotrauma MainThread = null; } - /// - /// Returns the file paths of all files of the given type in the content packages. - /// - /// - /// If true, also returns files in content packages that are installed but not currently selected. - public IEnumerable GetFilesOfType(ContentType type, bool searchAllContentPackages = false) - { - if (searchAllContentPackages) - { - return ContentPackage.GetFilesOfType(ContentPackage.AllPackages, type); - } - else - { - return ContentPackage.GetFilesOfType(Config.AllEnabledPackages, type); - } - } - public void OnInvitedToGame(Steamworks.Friend friend, string connectCommand) => OnInvitedToGame(connectCommand); public void OnInvitedToGame(string connectCommand) @@ -737,7 +624,7 @@ namespace Barotrauma if (SoundManager != null) { - if (WindowActive || !Config.MuteOnFocusLost) + if (WindowActive || !GameSettings.CurrentConfig.Audio.MuteOnFocusLost) { SoundManager.ListenerGain = SoundManager.CompressionDynamicRangeGain; } @@ -786,20 +673,23 @@ namespace Barotrauma CancelQuickStart = !CancelQuickStart; } - if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled || Config.TestScreenEnabled) && FirstLoad && !CancelQuickStart) + if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && + (GameSettings.CurrentConfig.AutomaticQuickStartEnabled || + GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled || + GameSettings.CurrentConfig.TestScreenEnabled) && FirstLoad && !CancelQuickStart) { loadingScreenOpen = false; FirstLoad = false; - if (Config.TestScreenEnabled) + if (GameSettings.CurrentConfig.TestScreenEnabled) { TestScreen.Select(); - } - else if (Config.AutomaticQuickStartEnabled) + } + else if (GameSettings.CurrentConfig.AutomaticQuickStartEnabled) { MainMenuScreen.QuickStart(); } - else if (Config.AutomaticCampaignLoadEnabled) + else if (GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled) { IEnumerable saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); @@ -822,12 +712,12 @@ namespace Barotrauma NetworkMember?.Update((float)Timing.Step); - if (!hasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) + if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) { throw new LoadingException(loadingCoroutine.Exception); } } - else if (hasLoaded) + else if (HasLoaded) { if (ConnectLobby != 0) { @@ -854,7 +744,7 @@ namespace Barotrauma GameMain.MainMenuScreen.Select(); } UInt64 serverSteamId = SteamManager.SteamIDStringToUInt64(ConnectEndpoint); - Client = new GameClient(Config.PlayerName, + Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), serverSteamId != 0 ? null : ConnectEndpoint, serverSteamId, string.IsNullOrWhiteSpace(ConnectName) ? ConnectEndpoint : ConnectName); @@ -888,9 +778,9 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - else if (Tutorial.Initialized && Tutorial.ContentRunning) + else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { - (GameSession.GameMode as TutorialMode).Tutorial.CloseActiveContentGUI(); + tutorialMode.Tutorial.CloseActiveContentGUI(); } else if (GameSession.IsTabMenuOpen) { @@ -935,8 +825,11 @@ namespace Barotrauma #endif GUI.ClearUpdateList(); - Paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || Tutorial.ContentRunning || DebugConsole.Paused) && - (NetworkMember == null || !NetworkMember.GameStarted); + Paused = + (DebugConsole.IsOpen || DebugConsole.Paused || + GUI.PauseMenuOpen || GUI.SettingsMenuOpen || + (GameSession?.GameMode is TutorialMode tutoMode && tutoMode.Tutorial.ContentRunning)) && + (NetworkMember == null || !NetworkMember.GameStarted); if (GameSession?.GameMode != null && GameSession.GameMode.Paused) { Paused = true; @@ -944,7 +837,7 @@ namespace Barotrauma } #if !DEBUG - if (NetworkMember == null && !WindowActive && !Paused && true && Config.PauseOnFocusLost && + if (NetworkMember == null && !WindowActive && !Paused && true && GameSettings.CurrentConfig.PauseOnFocusLost && Screen.Selected != MainMenuScreen && Screen.Selected != ServerListScreen && Screen.Selected != NetLobbyScreen && Screen.Selected != SubEditorScreen && Screen.Selected != LevelEditorScreen) { @@ -969,9 +862,9 @@ namespace Barotrauma { Screen.Selected.Update(Timing.Step); } - else if (Tutorial.Initialized && Tutorial.ContentRunning) + else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { - (GameSession.GameMode as TutorialMode).Update((float)Timing.Step); + tutorialMode.Update((float)Timing.Step); } else { @@ -988,6 +881,27 @@ namespace Barotrauma NetworkMember?.Update((float)Timing.Step); GUI.Update((float)Timing.Step); + +#if DEBUG + if (DebugDraw && GUI.MouseOn != null && PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.G)) + { + List hierarchy = new List(); + var currComponent = GUI.MouseOn; + while (currComponent != null) + { + hierarchy.Add(currComponent); + currComponent = currComponent.Parent; + } + DebugConsole.NewMessage("*********************"); + foreach (var component in hierarchy) + { + if (component is { MouseRect: var mouseRect, Rect: var rect }) + { + DebugConsole.NewMessage($"{component.GetType().Name} {component.Style?.Name ?? "[null]"} {rect.Bottom} {mouseRect.Bottom}", mouseRect!=rect ? Color.Lime : Color.Red); + } + } + } +#endif } CoroutineManager.Update((float)Timing.Step, Paused ? 0.0f : (float)Timing.Step); @@ -1035,7 +949,7 @@ namespace Barotrauma if (Timing.FrameLimit > 0) { double step = 1.0 / Timing.FrameLimit; - while (!Config.VSyncEnabled && sw.Elapsed.TotalSeconds + deltaTime < step) + while (!GameSettings.CurrentConfig.Graphics.VSync && sw.Elapsed.TotalSeconds + deltaTime < step) { Thread.Sleep(1); } @@ -1047,7 +961,7 @@ namespace Barotrauma { TitleScreen.Draw(spriteBatch, base.GraphicsDevice, (float)deltaTime); } - else if (hasLoaded) + else if (HasLoaded) { Screen.Selected.Draw(deltaTime, base.GraphicsDevice, spriteBatch); } @@ -1055,8 +969,33 @@ namespace Barotrauma if (DebugDraw && GUI.MouseOn != null) { spriteBatch.Begin(); - GUI.DrawRectangle(spriteBatch, GUI.MouseOn.MouseRect, Color.Lime); - GUI.DrawRectangle(spriteBatch, GUI.MouseOn.Rect, Color.Cyan); + if (PlayerInput.IsCtrlDown() && PlayerInput.KeyDown(Keys.G)) + { + List hierarchy = new List(); + var currComponent = GUI.MouseOn; + while (currComponent != null) + { + hierarchy.Add(currComponent); + currComponent = currComponent.Parent; + } + + Color[] colors = { Color.Lime, Color.Yellow, Color.Aqua, Color.Red }; + for (int index = 0; index < hierarchy.Count; index++) + { + var component = hierarchy[index]; + if (component is { MouseRect: var mouseRect, Rect: var rect }) + { + if (mouseRect.IsEmpty) { mouseRect = rect; } + mouseRect.Location += (index%2,(index%4)/2); + GUI.DrawRectangle(spriteBatch, mouseRect, colors[index%4]); + } + } + } + else + { + GUI.DrawRectangle(spriteBatch, GUI.MouseOn.MouseRect, Color.Lime); + GUI.DrawRectangle(spriteBatch, GUI.MouseOn.Rect, Color.Cyan); + } spriteBatch.End(); } @@ -1071,7 +1010,7 @@ namespace Barotrauma if (showVerificationPrompt) { string text = (Screen.Selected is CharacterEditor.CharacterEditorScreen || Screen.Selected is SubEditorScreen) ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification"; - var msgBox = new GUIMessageBox("", TextManager.Get(text), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + var msgBox = new GUIMessageBox("", TextManager.Get(text), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { UserData = "verificationprompt" }; @@ -1119,18 +1058,18 @@ namespace Barotrauma { double roundDuration = Timing.TotalTime - GameSession.RoundStartTime; GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, - GameSession.GameMode?.Preset.Identifier ?? "none", + GameSession.GameMode?.Preset.Identifier.Value ?? "none", roundDuration); - string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier ?? "none") + ":"; + string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); foreach (var activeEvent in GameSession.EventManager.ActiveEvents) { GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:ActiveEvents:" + activeEvent.ToString()); } GameSession.LogEndRoundStats(eventId); - if (Tutorial.Initialized) + if (GameSession.GameMode is TutorialMode tutorialMode) { - ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); + tutorialMode.Tutorial?.Stop(); } } GUIMessageBox.CloseAll(); @@ -1142,7 +1081,7 @@ namespace Barotrauma public void ShowCampaignDisclaimer(Action onContinue = null) { var msgBox = new GUIMessageBox(TextManager.Get("CampaignDisclaimerTitle"), TextManager.Get("CampaignDisclaimerText"), - new string[] { TextManager.Get("CampaignRoadMapTitle"), TextManager.Get("OK") }); + new LocalizedString[] { TextManager.Get("CampaignRoadMapTitle"), TextManager.Get("OK") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { @@ -1153,8 +1092,10 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked += (_, __) => { onContinue?.Invoke(); return true; }; - Config.CampaignDisclaimerShown = true; - Config.SaveNewPlayerConfig(); + var config = GameSettings.CurrentConfig; + config.CampaignDisclaimerShown = true; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); } public void ShowEditorDisclaimer() @@ -1162,16 +1103,16 @@ namespace Barotrauma var msgBox = new GUIMessageBox(TextManager.Get("EditorDisclaimerTitle"), TextManager.Get("EditorDisclaimerText")); var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); - List> links = new List>() + List<(LocalizedString Caption, LocalizedString Url)> links = new List<(LocalizedString, LocalizedString)>() { - new Pair(TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl")), - new Pair(TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl")), + (TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl")), + (TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl")), }; foreach (var link in links) { - new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), linkHolder.RectTransform), link.First, style: "MainMenuGUIButton", textAlignment: Alignment.Left) + new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), linkHolder.RectTransform), link.Caption, style: "MainMenuGUIButton", textAlignment: Alignment.Left) { - UserData = link.Second, + UserData = link.Url, OnClicked = (btn, userdata) => { ShowOpenUrlInWebBrowserPrompt(userdata as string); @@ -1182,8 +1123,10 @@ namespace Barotrauma msgBox.InnerFrame.RectTransform.MinSize = new Point(0, msgBox.InnerFrame.Rect.Height + linkHolder.Rect.Height + msgBox.Content.AbsoluteSpacing * 2 + 10); - Config.EditorDisclaimerShown = true; - Config.SaveNewPlayerConfig(); + var config = GameSettings.CurrentConfig; + config.EditorDisclaimerShown = true; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); } public void ShowBugReporter() @@ -1257,7 +1200,8 @@ namespace Barotrauma } if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } base.OnExiting(sender, args); } @@ -1267,14 +1211,14 @@ namespace Barotrauma if (string.IsNullOrEmpty(url)) { return; } if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } - string text = TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url); - string extensionText = TextManager.Get(promptExtensionTag, returnNull: true, useEnglishAsFallBack: false); - if (!string.IsNullOrEmpty(extensionText)) + LocalizedString text = TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url); + LocalizedString extensionText = TextManager.Get(promptExtensionTag); + if (!extensionText.IsNullOrEmpty()) { text += $"\n\n{extensionText}"; } - var msgBox = new GUIMessageBox("", text, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + var msgBox = new GUIMessageBox("", text, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 2d8f290ab..f25e1db11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -170,7 +170,7 @@ namespace Barotrauma var matchingItem = matchingItems.ElementAt(i); SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); SoldEntities.Add(new SoldEntity(matchingItem, campaign.IsSinglePlayer ? SoldEntity.SellStatus.Confirmed : SoldEntity.SellStatus.Local)); - if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } + if (canAddToRemoveQueue) { Entity.Spawner.AddItemToRemoveQueue(matchingItem); } } } else @@ -185,7 +185,7 @@ namespace Barotrauma // Exchange money Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; - GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); // Remove from the sell crate if ((sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 48f599318..86d82528c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -17,7 +17,7 @@ namespace Barotrauma { private Point screenResolution; - public Order DraggedOrder; + public OrderPrefab DraggedOrderPrefab; public bool DragOrder; private bool dropOrder; private int framesToSkip = 2; @@ -54,7 +54,8 @@ namespace Barotrauma set { if (_isCrewMenuOpen == value) { return; } - _isCrewMenuOpen = GameMain.Config.CrewMenuOpen = value; + _isCrewMenuOpen = value; + #warning TODO: update GameSettings.CurrentConfig.CrewMenuOpen when round ends } } @@ -62,7 +63,7 @@ namespace Barotrauma public void AutoHideCrewList() => _isCrewMenuOpen = false; - public void ResetCrewList() => _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; + public void ResetCrewList() => _isCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen; const float CommandNodeAnimDuration = 0.2f; @@ -192,7 +193,7 @@ namespace Barotrauma }; } - List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -200,7 +201,7 @@ namespace Barotrauma } ReportButtonFrame = new GUILayoutGroup(new RectTransform( - new Point((HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height - (int)((reports.Count - 1) * 5 * GUI.Scale)) / reports.Count, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height), guiFrame.RectTransform)) + new Point((HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height - (int)((reports.Length - 1) * 5 * GUI.Scale)) / reports.Length, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height), guiFrame.RectTransform)) { AbsoluteSpacing = (int)(5 * GUI.Scale), UserData = "reportbuttons", @@ -216,42 +217,42 @@ namespace Barotrauma screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevUIScale = GUI.Scale; - _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; - dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); + _isCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen; } - public static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, List reports, bool isHorizontal) + public static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, IReadOnlyList reports, bool isHorizontal) { //report buttons - foreach (Order order in reports) + foreach (OrderPrefab orderPrefab in reports) { - if (!order.IsReport || order.SymbolSprite == null || order.Hidden) { continue; } - var btn = new GUIButton(new RectTransform(new Point(isHorizontal ? parent.Rect.Height : parent.Rect.Width), parent.RectTransform), style: null) + if (!orderPrefab.IsReport || orderPrefab.SymbolSprite == null || orderPrefab.Hidden) { continue; } + var btn = new GUIButton(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: isHorizontal ? ScaleBasis.BothHeight : ScaleBasis.BothWidth), style: null) { OnClicked = (button, userData) => { - if (!CanIssueOrders || crewManager?.DraggedOrder != null) { return false; } + if (!CanIssueOrders || crewManager?.DraggedOrderPrefab != null) { return false; } var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } if (crewManager != null) { - crewManager.SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + Order order = new Order(orderPrefab, Identifier.Empty, CharacterInfo.HighestManualOrderPriority, Order.OrderType.Current, null, null, orderGiver: Character.Controlled); + crewManager.SetCharacterOrder(null, order); if (crewManager.IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } } return true; }, - UserData = order, + UserData = orderPrefab, ClampMouseRectToParent = false }; - btn.ToolTip = $"‖color:{XMLExtensions.ColorToString(order.Prefab.Color)}‖{order.Name}‖color:end‖\n{TextManager.Get("draganddropreports")}"; + btn.ToolTip = RichString.Rich($"‖color:{XMLExtensions.ColorToString(orderPrefab.Color)}‖{orderPrefab.Name}‖color:end‖\n{TextManager.Get("draganddropreports")}"); if (crewManager != null) { btn.OnButtonDown = () => { crewManager.dragOrderTreshold = Math.Max(btn.Rect.Width, btn.Rect.Height) / 2f; - crewManager.DraggedOrder = order; + crewManager.DraggedOrderPrefab = orderPrefab; crewManager.dropOrder = false; crewManager.framesToSkip = 2; crewManager.dragPoint = btn.Rect.Center.ToVector2(); @@ -261,21 +262,21 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular") { - Color = GUI.Style.Red * 0.8f, - HoverColor = GUI.Style.Red * 1.0f, - PressedColor = GUI.Style.Red * 0.6f, + Color = GUIStyle.Red * 0.8f, + HoverColor = GUIStyle.Red * 1.0f, + PressedColor = GUIStyle.Red * 0.6f, UserData = "highlighted", CanBeFocused = false, Visible = false }; - var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), order.Prefab.SymbolSprite, scaleToFit: true) + var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), orderPrefab.SymbolSprite, scaleToFit: true) { - Color = order.Color, - HoverColor = Color.Lerp(order.Color, Color.White, 0.5f), - ToolTip = btn.RawToolTip, + Color = orderPrefab.Color, + HoverColor = Color.Lerp(orderPrefab.Color, Color.White, 0.5f), + ToolTip = btn.ToolTip, SpriteEffects = SpriteEffects.FlipHorizontally, - UserData = order + UserData = orderPrefab }; } } @@ -402,7 +403,7 @@ namespace Barotrauma // Spacing - (7 * layoutGroup.RelativeSpacing); - var font = layoutGroup.Rect.Width < 150 ? GUI.SmallFont : GUI.Font; + var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font; var nameBlock = new GUITextBlock( new RectTransform( new Vector2(nameRelativeWidth, 1.0f), @@ -476,7 +477,7 @@ namespace Barotrauma }; new GUIImage( new RectTransform(Vector2.One, soundIconParent.RectTransform), - GUI.Style.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), + GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { CanBeFocused = false, @@ -521,14 +522,13 @@ namespace Barotrauma { if (!(characterComponent?.UserData is Character character)) { return; } if (character.Info?.Job?.Prefab == null) { return; } - string tooltip = TextManager.GetWithVariables("crewlistelementtooltip", - new string[] { "[name]", "[job]" }, - new string[] { character.Name, character.Info.Job.Name }); + + LocalizedString tooltip = TextManager.GetWithVariables("crewlistelementtooltip", + ("[name]", character.Name), + ("[job]", character.Info.Job.Name)); string color = XMLExtensions.ColorToString(character.Info.Job.Prefab.UIColor); - tooltip = $"‖color:{color}‖{tooltip}‖color:end‖"; - var richTextData = RichTextData.GetRichTextData(tooltip, out string sanitizedTooltip); - characterComponent.ToolTip = sanitizedTooltip; - characterComponent.TooltipRichTextData = richTextData; + RichString richToolTip = RichString.Rich($"‖color:{color}‖"+tooltip+"‖color:end‖"); + characterComponent.ToolTip = richToolTip; } /// @@ -662,6 +662,10 @@ namespace Barotrauma /// /// Adds the message to the single player chatbox. /// + public void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Character sender) + { + AddSinglePlayerChatMessage(senderName.Value, text.Value, messageType, sender); + } public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Character sender) { if (!IsSinglePlayer) @@ -767,16 +771,16 @@ namespace Barotrauma /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null, bool isNewOrder = true) + public void SetCharacterOrder(Character character, Order order, bool isNewOrder = true) { if (order != null && order.TargetAllCharacters) { - Hull hull = targetHull; + Hull hull = order.TargetHull; if (order.IsReport) { - if (orderGiver?.CurrentHull == null && hull == null) { return; } - hull ??= orderGiver.CurrentHull; - AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); + if (order.OrderGiver?.CurrentHull == null && hull == null) { return; } + hull ??= order.OrderGiver.CurrentHull; + AddOrder(order.WithTargetEntity(hull), order.FadeOutTime); } else if (order.IsIgnoreOrder) { @@ -784,7 +788,7 @@ namespace Barotrauma if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is IIgnorable ignorable) { ignorable.OrderedToBeIgnored = order.Identifier == "ignorethis"; - AddOrder(new Order(order.Prefab ?? order, order.TargetEntity, order.TargetItemComponent, orderGiver), null); + AddOrder(order.Clone(), null); } else if (order.TargetType == Order.OrderTargetType.WallSection && order.TargetEntity is Structure s) { @@ -793,7 +797,7 @@ namespace Barotrauma if (ws != null) { ws.OrderedToBeIgnored = order.Identifier == "ignorethis"; - AddOrder(new Order(order.Prefab ?? order, s, wallSectionIndex, orderGiver), null); + AddOrder(order.WithWallSection(s, wallSectionIndex), null); } } else @@ -817,11 +821,11 @@ namespace Barotrauma if (IsSinglePlayer) { - orderGiver.Speak(order.GetChatMessage("", hull?.DisplayName, givingOrderToSelf: character == orderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); + order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); } else { - OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver, isNewOrder: isNewOrder); + OrderChatMessage msg = new OrderChatMessage(order.WithTargetEntity(order.IsReport ? hull : order.TargetEntity), null, order.OrderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -830,15 +834,16 @@ namespace Barotrauma //can't issue an order if no characters are available if (character == null) { return; } + var orderGiver = order?.OrderGiver; if (IsSinglePlayer) { - character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); - string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, isNewOrder: isNewOrder); + character.SetOrder(order, speak: orderGiver != character); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: character == orderGiver, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); orderGiver?.Speak(message); } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item, character, orderGiver, isNewOrder: isNewOrder); + OrderChatMessage msg = new OrderChatMessage(order, character, orderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -847,7 +852,7 @@ namespace Barotrauma /// /// Displays the specified order in the crew UI next to the character. /// - public void AddCurrentOrderIcon(Character character, Order order, string option, int priority) + public void AddCurrentOrderIcon(Character character, Order order) { if (character == null) { return; } @@ -858,25 +863,25 @@ namespace Barotrauma var currentOrderIconList = GetCurrentOrderIconList(characterComponent); var currentOrderIcons = currentOrderIconList.Content.Children; var iconsToRemove = new List(); - var newPreviousOrders = new List(); + var newPreviousOrders = new List(); bool updatedExistingIcon = false; foreach (var icon in currentOrderIcons) { - var orderInfo = (OrderInfo)icon.UserData; - var matchingOrder = character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption); - if (!matchingOrder.HasValue) + var orderInfo = (Order)icon.UserData; + var matchingOrder = character.GetCurrentOrder(orderInfo); + if (matchingOrder is null) { iconsToRemove.Add(icon); newPreviousOrders.Add(orderInfo); } - else if (orderInfo.MatchesOrder(order, option)) + else if (orderInfo.MatchesOrder(order)) { - icon.UserData = new OrderInfo(order, option, priority); + icon.UserData = order.Clone(); if (icon is GUIImage image) { - image.Sprite = GetOrderIconSprite(order, option); - image.ToolTip = CreateOrderTooltip(order, option); + image.Sprite = GetOrderIconSprite(order); + image.ToolTip = CreateOrderTooltip(order); } updatedExistingIcon = true; } @@ -889,8 +894,8 @@ namespace Barotrauma var previousOrderIcons = previousOrderIconGroup.Children; foreach (var icon in previousOrderIcons) { - var orderInfo = (OrderInfo)icon.UserData; - if (orderInfo.MatchesOrder(order, option)) + var orderInfo = (Order)icon.UserData; + if (orderInfo.MatchesOrder(order)) { previousOrderIconGroup.RemoveChild(icon); break; @@ -922,15 +927,14 @@ namespace Barotrauma float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * currentOrderIconList.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); Point size = new Point((int)nodeWidth, currentOrderIconList.RectTransform.NonScaledSize.Y); - var nodeIcon = CreateNodeIcon(size, currentOrderIconList.Content.RectTransform, GetOrderIconSprite(order, option), order.Color, tooltip: CreateOrderTooltip(order, option)); - nodeIcon.UserData = new OrderInfo(order, option, priority); + var nodeIcon = CreateNodeIcon(size, currentOrderIconList.Content.RectTransform, GetOrderIconSprite(order), order.Color, tooltip: CreateOrderTooltip(order)); + nodeIcon.UserData = order.Clone(); nodeIcon.OnSecondaryClicked = (image, userData) => { if (!CanIssueOrders) { return false; } - var orderInfo = (OrderInfo)userData; - SetCharacterOrder(character, dismissedOrderPrefab, Order.GetDismissOrderOption(orderInfo), - character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption)?.ManualPriority ?? 0, - Character.Controlled); + var orderInfo = (Order)userData; + var order = orderInfo.GetDismissal().WithManualPriority(character.GetCurrentOrder(orderInfo)?.ManualPriority ?? 0).WithOrderGiver(Character.Controlled); + SetCharacterOrder(character, order); return true; }; @@ -942,7 +946,7 @@ namespace Barotrauma Visible = false }; - int hierarchyIndex = Math.Clamp(CharacterInfo.HighestManualOrderPriority - priority, 0, Math.Max(currentOrderIconList.Content.CountChildren - 1, 0)); + int hierarchyIndex = Math.Clamp(CharacterInfo.HighestManualOrderPriority - order.ManualPriority, 0, Math.Max(currentOrderIconList.Content.CountChildren - 1, 0)); if (hierarchyIndex != currentOrderIconList.Content.GetChildIndex(nodeIcon)) { nodeIcon.RectTransform.RepositionChildInHierarchy(hierarchyIndex); @@ -957,21 +961,21 @@ namespace Barotrauma // Make sure priority values are up-to-date foreach (var currentOrderInfo in character.CurrentOrders) { - var component = currentOrderIconList.Content.FindChild(c => c?.UserData is OrderInfo componentOrderInfo && + var component = currentOrderIconList.Content.FindChild(c => c?.UserData is Order componentOrderInfo && componentOrderInfo.MatchesOrder(currentOrderInfo)); if (component == null) { continue; } - var componentOrderInfo = (OrderInfo)component.UserData; + var componentOrderInfo = (Order)component.UserData; int newPriority = currentOrderInfo.ManualPriority; if (componentOrderInfo.ManualPriority != newPriority) { - component.UserData = new OrderInfo(componentOrderInfo, newPriority); + component.UserData = componentOrderInfo.WithManualPriority(newPriority); } } currentOrderIconList.Content.RectTransform.SortChildren((x, y) => { - var xOrder = (OrderInfo)x.GUIComponent.UserData; - var yOrder = (OrderInfo)y.GUIComponent.UserData; + var xOrder = (Order)x.GUIComponent.UserData; + var yOrder = (Order)y.GUIComponent.UserData; return yOrder.ManualPriority.CompareTo(xOrder.ManualPriority); }); @@ -987,14 +991,9 @@ namespace Barotrauma } } - public void AddCurrentOrderIcon(Character character, OrderInfo? orderInfo) + private void AddPreviousOrderIcon(Character character, GUIComponent characterComponent, Order orderInfo) { - AddCurrentOrderIcon(character, orderInfo?.Order, orderInfo?.OrderOption, orderInfo?.ManualPriority ?? 0); - } - - private void AddPreviousOrderIcon(Character character, GUIComponent characterComponent, OrderInfo orderInfo) - { - if (orderInfo.Order == null || orderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } + if (orderInfo == null || orderInfo.Identifier == dismissedOrderPrefab.Identifier) { return; } var currentOrderIconList = GetCurrentOrderIconList(characterComponent); int maxPreviousOrderIcons = CharacterInfo.MaxCurrentOrders - currentOrderIconList.Content.CountChildren; @@ -1009,16 +1008,16 @@ namespace Barotrauma float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * previousOrderIconGroup.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); Point size = new Point((int)nodeWidth, previousOrderIconGroup.Rect.Height); - var previousOrderInfo = new OrderInfo(orderInfo, OrderInfo.OrderType.Previous); + var previousOrderInfo = orderInfo.WithType(Order.OrderType.Previous); var prevOrderFrame = new GUIButton(new RectTransform(size, parent: previousOrderIconGroup.RectTransform), style: null) { UserData = previousOrderInfo, OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } - var orderInfo = (OrderInfo)userData; - int priority = GetManualOrderPriority(character, orderInfo.Order); - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); + var orderInfo = (Order)userData; + int priority = GetManualOrderPriority(character, orderInfo); + SetCharacterOrder(character, orderInfo.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); return true; }, OnSecondaryClicked = (button, userData) => @@ -1038,7 +1037,7 @@ namespace Barotrauma CreateNodeIcon(Vector2.One, prevOrderIconFrame.RectTransform, GetOrderIconSprite(previousOrderInfo), - previousOrderInfo.Order.Color, + previousOrderInfo.Color, tooltip: CreateOrderTooltip(previousOrderInfo)); foreach (GUIComponent c in prevOrderIconFrame.Children) @@ -1071,7 +1070,7 @@ namespace Barotrauma { foreach (GUIComponent icon in oldPrevOrderIcons) { - if (icon.UserData is OrderInfo orderInfo) + if (icon.UserData is Order orderInfo) { AddPreviousOrderIcon(character, newCharacterComponent, orderInfo); } @@ -1116,33 +1115,27 @@ namespace Barotrauma { var orderComponent = orderList.Content.GetChildByUserData(userData); if (orderComponent == null) { return; } - var orderInfo = (OrderInfo)userData; + var orderInfo = (Order)userData; var priority = Math.Max(CharacterInfo.HighestManualOrderPriority - orderList.Content.GetChildIndex(orderComponent), 1); if (orderInfo.ManualPriority == priority) { return; } var character = (Character)orderList.UserData; - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled, isNewOrder: false); + SetCharacterOrder(character, orderInfo.WithManualPriority(priority), isNewOrder: false); } - private string CreateOrderTooltip(Order orderPrefab, string option, Entity targetEntity) + private LocalizedString CreateOrderTooltip(OrderPrefab orderPrefab, Identifier option, Entity targetEntity) { if (orderPrefab == null) { return ""; } - if (orderPrefab.DisplayGiverInTooltip && orderPrefab.OrderGiver != null) + if (option != Identifier.Empty) { - return TextManager.GetWithVariables("crewlistordericontooltip", - new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { orderPrefab.Name, orderPrefab.OrderGiver.DisplayName }); - } - else if (!string.IsNullOrEmpty(option)) - { - return TextManager.GetWithVariables("crewlistordericontooltip", - new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { orderPrefab.Name, orderPrefab.GetOptionName(option) }); + return TextManager.GetWithVariables("crewlistordericontooltip".ToIdentifier(), + ("[ordername]".ToIdentifier(), orderPrefab.Name), + ("[orderoption]".ToIdentifier(), orderPrefab.GetOptionName(option))); } else if (targetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null) { - return TextManager.GetWithVariables("crewlistordericontooltip", - new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { orderPrefab.Name, targetItem.Name }); + return TextManager.GetWithVariables("crewlistordericontooltip".ToIdentifier(), + ("[ordername]".ToIdentifier(), orderPrefab.Name), + ("[orderoption]".ToIdentifier(), targetItem.Name)); } else { @@ -1150,23 +1143,24 @@ namespace Barotrauma } } - private string CreateOrderTooltip(Order order, string option) + private LocalizedString CreateOrderTooltip(Order order) { - return CreateOrderTooltip(order?.Prefab ?? order, option, order?.TargetEntity); + if (order.DisplayGiverInTooltip && order.OrderGiver != null) + { + return TextManager.GetWithVariables("crewlistordericontooltip", + ("[ordername]", order.Name), + ("[orderoption]", order.OrderGiver.DisplayName)); + } + return CreateOrderTooltip(order.Prefab, order.Option, order?.TargetEntity); } - private string CreateOrderTooltip(OrderInfo orderInfo) - { - return CreateOrderTooltip(orderInfo.Order?.Prefab ?? orderInfo.Order, orderInfo.OrderOption, orderInfo.Order?.TargetEntity); - } - - private Sprite GetOrderIconSprite(Order order, string option) + private Sprite GetOrderIconSprite(Order order) { if (order == null) { return null; } Sprite sprite = null; - if (option != null && order.Prefab.OptionSprites.Any()) + if (order.Option != Identifier.Empty && order.Prefab.OptionSprites.Any()) { - order.Prefab.OptionSprites.TryGetValue(option, out sprite); + order.Prefab.OptionSprites.TryGetValue(order.Option, out sprite); } if (sprite == null && order.TargetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null) { @@ -1175,9 +1169,6 @@ namespace Barotrauma return sprite ?? order.SymbolSprite; } - private Sprite GetOrderIconSprite(OrderInfo orderInfo) => - GetOrderIconSprite(orderInfo.Order, orderInfo.OrderOption); - #endregion #region Updating and drawing the UI @@ -1208,7 +1199,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: 10); } } - + public void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } @@ -1297,13 +1288,15 @@ namespace Barotrauma return 0; } - private bool CreateOrder(Order order, Hull targetHull = null) + private bool CreateOrder(OrderPrefab orderPrefab, Hull targetHull = null) { var sub = Character.Controlled?.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } - SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled, targetHull); + var order = new Order(orderPrefab, targetHull, null, Character.Controlled) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + SetCharacterOrder(null, order); if (IsSinglePlayer) { @@ -1315,7 +1308,7 @@ namespace Barotrauma private void UpdateOrderDrag() { - if (DraggedOrder is { } order) + if (DraggedOrderPrefab is { } orderPrefab) { if (dropOrder) { @@ -1342,12 +1335,12 @@ namespace Barotrauma framesToSkip = 2; dropOrder = false; - DraggedOrder = null; + DraggedOrderPrefab = null; if (hull is null && GUI.MouseOn is { Visible: true, CanBeFocused: true }) { return; } - hull ??= Hull.hullList.FirstOrDefault(h => h.WorldRect.ContainsWorld(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition))); - CreateOrder(order, hull); + hull ??= Hull.HullList.FirstOrDefault(h => h.WorldRect.ContainsWorld(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition))); + CreateOrder(orderPrefab, hull); } } else @@ -1362,7 +1355,7 @@ namespace Barotrauma } else { - DraggedOrder = null; + DraggedOrderPrefab = null; } dragPoint = Vector2.Zero; DragOrder = false; @@ -1423,12 +1416,12 @@ namespace Barotrauma // When using Deselect to close the interface, make sure it's not a seconday mouse button click on a node // That should be reserved for opening manual assignment - bool isMouseOnOptionNode = optionNodes.Any(n => GUI.IsMouseOn(n.Item1)); + bool isMouseOnOptionNode = optionNodes.Any(n => GUI.IsMouseOn(n.Button)); bool isMouseOnShortcutNode = !isMouseOnOptionNode && shortcutNodes.Any(n => GUI.IsMouseOn(n)); bool hitDeselect = PlayerInput.KeyHit(InputType.Deselect) && (!PlayerInput.SecondaryMouseButtonClicked() || (!isMouseOnOptionNode && !isMouseOnShortcutNode)); - bool isBoundToPrimaryMouse = GameMain.Config.KeyBind(InputType.Command).MouseButton is MouseButton mouseButton && + bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton is MouseButton mouseButton && (mouseButton == MouseButton.PrimaryMouse || mouseButton == (PlayerInput.MouseButtonsSwapped() ? MouseButton.RightMouse : MouseButton.LeftMouse)); bool canToggleInterface = !isBoundToPrimaryMouse || (!isMouseOnOptionNode && !isMouseOnShortcutNode && extraOptionNodes.None(n => GUI.IsMouseOn(n)) && !GUI.IsMouseOn(returnNode)); @@ -1467,7 +1460,7 @@ namespace Barotrauma GUIComponent closestNode = null; float closestBearing = 0; - optionNodes.ForEach(n => CheckIfClosest(n.Item1)); + optionNodes.ForEach(n => CheckIfClosest(n.Button)); CheckIfClosest(returnNode); void CheckIfClosest(GUIComponent comp) @@ -1524,18 +1517,18 @@ namespace Barotrauma } var hotkeyHit = false; - foreach (Tuple node in optionNodes) + foreach (OptionNode node in optionNodes) { - if (node.Item2 != Keys.None && PlayerInput.KeyHit(node.Item2)) + if (node.Keys != Keys.None && PlayerInput.KeyHit(node.Keys)) { - var b = node.Item1 as GUIButton; + var b = node.Button as GUIButton; if (PlayerInput.IsShiftDown() && b?.OnSecondaryClicked != null) { - b.OnSecondaryClicked.Invoke(node.Item1 as GUIButton, node.Item1.UserData); + b.OnSecondaryClicked.Invoke(node.Button as GUIButton, node.Button.UserData); } else { - b?.OnClicked?.Invoke(node.Item1 as GUIButton, node.Item1.UserData); + b?.OnClicked?.Invoke(node.Button as GUIButton, node.Button.UserData); } ResetNodeSelection(); hotkeyHit = true; @@ -1630,8 +1623,7 @@ namespace Barotrauma { foreach (var orderIcon in currentOrderIconList.Content.Children) { - if (!(orderIcon.UserData is OrderInfo orderInfo)) { continue; } - if (!(orderInfo.Order is Order order)) { continue; } + if (!(orderIcon.UserData is Order order)) { continue; } if (order.ColoredWhenControllingGiver && order.OrderGiver != Character.Controlled) { orderIcon.Color = AIObjective.ObjectiveIconColor; @@ -1652,7 +1644,10 @@ namespace Barotrauma if (objectiveManager.IsOrder(currentObjective)) { var orderInfo = objectiveManager.CurrentOrders.FirstOrDefault(o => o.Objective == currentObjective); - SetOrderHighlight(characterComponent, orderInfo.Order?.Identifier, orderInfo.OrderOption); + if (orderInfo != null) + { + SetOrderHighlight(characterComponent, orderInfo.Identifier, orderInfo.Option); + } } else { @@ -1706,7 +1701,7 @@ namespace Barotrauma UpdateReports(); } - private void SetOrderHighlight(GUIComponent characterComponent, string orderIdentifier, string orderOption) + private void SetOrderHighlight(GUIComponent characterComponent, Identifier orderIdentifier, Identifier orderOption) { if (characterComponent == null) { return; } RemoveObjectiveIcon(characterComponent); @@ -1722,14 +1717,14 @@ namespace Barotrauma glowComponent.Visible = false; continue; } - var orderInfo = (OrderInfo)orderIcon.UserData; + var orderInfo = (Order)orderIcon.UserData; foundMatch = orderInfo.MatchesOrder(orderIdentifier, orderOption); glowComponent.Visible = foundMatch; } } } - public void SetOrderHighlight(Character character, string orderIdentifier, string orderOption) + public void SetOrderHighlight(Character character, Identifier orderIdentifier, Identifier orderOption) { if (crewList == null) { return; } var characterComponent = crewList.Content.GetChildByUserData(character); @@ -1749,7 +1744,7 @@ namespace Barotrauma } } - private void CreateObjectiveIcon(GUIComponent characterComponent, Sprite sprite, string tooltip) + private void CreateObjectiveIcon(GUIComponent characterComponent, Sprite sprite, LocalizedString tooltip) { if (characterComponent == null || !(characterComponent.UserData is Character character) || character.IsPlayer) { return; } DisableOrderHighlight(characterComponent); @@ -1777,7 +1772,7 @@ namespace Barotrauma } } - public void CreateObjectiveIcon(Character character, string identifier, string option, Entity targetEntity) + public void CreateObjectiveIcon(Character character, Identifier identifier, Identifier option, Entity targetEntity) { CreateObjectiveIcon(crewList?.Content.GetChildByUserData(character), AIObjective.GetSprite(identifier, option, targetEntity), @@ -1791,22 +1786,22 @@ namespace Barotrauma GetObjectiveIconTooltip(objective)); } - private string GetObjectiveIconTooltip(string identifier, string option, Entity targetEntity) + private LocalizedString GetObjectiveIconTooltip(Identifier identifier, Identifier option, Entity targetEntity) { - string variableValue; - identifier = identifier.RemoveWhitespace(); - if (Order.Prefabs.TryGetValue(identifier, out Order orderPrefab)) + LocalizedString variableValue; + if (OrderPrefab.Prefabs.ContainsKey(identifier)) { + var orderPrefab = OrderPrefab.Prefabs[identifier]; variableValue = CreateOrderTooltip(orderPrefab, option, targetEntity); } else { - variableValue = TextManager.Get($"objective.{identifier}", returnNull: true) ?? ""; + variableValue = TextManager.Get($"objective.{identifier}"); } - return string.IsNullOrEmpty(variableValue) ? variableValue : TextManager.GetWithVariable("crewlistobjectivetooltip", "[objective]", variableValue); + return variableValue.IsNullOrEmpty() ? variableValue : TextManager.GetWithVariable("crewlistobjectivetooltip", "[objective]", variableValue); } - private string GetObjectiveIconTooltip(AIObjective objective) + private LocalizedString GetObjectiveIconTooltip(AIObjective objective) { return objective == null ? "" : GetObjectiveIconTooltip(objective.Identifier, objective.Option, (objective as AIObjectiveOperateItem)?.OperateTarget); @@ -1850,7 +1845,17 @@ namespace Barotrauma private GUIFrame commandFrame, targetFrame; private GUIButton centerNode, returnNode, expandNode; private GUIFrame shortcutCenterNode; - private readonly List> optionNodes = new List>(); + private class OptionNode + { + public readonly GUIButton Button; + public readonly Keys Keys; + public OptionNode(GUIButton guiComponent, Keys keys) + { + Button = guiComponent; + Keys = keys; + } + } + private readonly List optionNodes = new List(); private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None; private readonly List shortcutNodes = new List(); private readonly List extraOptionNodes = new List(); @@ -1875,13 +1880,13 @@ namespace Barotrauma private const float nodeColorMultiplier = 0.75f; private int nodeDistance = (int)(GUI.Scale * 250); private const float returnNodeDistanceModifier = 0.65f; - private Order dismissedOrderPrefab; + private OrderPrefab dismissedOrderPrefab => OrderPrefab.Dismissal; private Character characterContext; private Item itemContext; private Hull hullContext; private WallSection wallContext; private bool isContextual; - private readonly List contextualOrders = new List(); + private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; private const int maxShortcutNodeCount = 4; @@ -2033,7 +2038,6 @@ namespace Barotrauma SetCenterNode(startNode); availableCategories ??= GetAvailableCategories(); - dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); if (isContextual) { @@ -2095,7 +2099,7 @@ namespace Barotrauma availableCategories = new List(); foreach (OrderCategory category in Enum.GetValues(typeof(OrderCategory))) { - if (Order.PrefabList.Any(o => o.Category == category && !o.IsReport)) + if (OrderPrefab.Prefabs.Any(o => o.Category == category && !o.IsReport)) { availableCategories.Add(category); } @@ -2120,24 +2124,24 @@ namespace Barotrauma if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); // Don't draw connectors for assignment nodes - if (!(optionNodes.FirstOrDefault()?.Item1.UserData is Character)) + if (!(optionNodes.FirstOrDefault()?.Button.UserData is Character)) { // Regular option nodes if (targetFrame == null || !targetFrame.Visible) { - optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Item1, optionNodeMargin, spriteBatch)); + optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Button, optionNodeMargin, spriteBatch)); } - // Minimap item nodes for single-option orders - else if(optionNodes.FirstOrDefault()?.Item1?.UserData is Tuple userData && string.IsNullOrEmpty(userData.Item2)) + // Minimap item nodes + else { foreach (var node in optionNodes) { float iconRadius = 0.5f * optionNodeMargin; - Vector2 itemPosition = node.Item1.Parent.Rect.Center.ToVector2(); - if (Vector2.Distance(node.Item1.Center, itemPosition) <= iconRadius) { continue; } - DrawNodeConnector(itemPosition, 0.0f, node.Item1, iconRadius, spriteBatch, widthMultiplier: 0.5f); + Vector2 itemPosition = node.Button.Parent.Rect.Center.ToVector2(); + if (Vector2.Distance(node.Button.Center, itemPosition) <= iconRadius) { continue; } + DrawNodeConnector(itemPosition, 0.0f, node.Button, iconRadius, spriteBatch, widthMultiplier: 0.5f); GUI.DrawFilledRectangle(spriteBatch, itemPosition - Vector2.One, new Vector2(3), - node.Item1.GetChildByUserData("colorsource")?.Color ?? Color.White); + node.Button.GetChildByUserData("colorsource")?.Color ?? Color.White); } } } @@ -2199,7 +2203,7 @@ namespace Barotrauma private bool NavigateForward(GUIButton node, object userData) { if (commandFrame == null) { return false; } - if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) + if (!(optionNodes.Find(n => n.Button == node) is OptionNode optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; @@ -2367,7 +2371,7 @@ namespace Barotrauma CreateOrderOptionNodes(nodeOrder, itemContext ?? nodeOrder.TargetEntity as Item ?? matchingItems?.FirstOrDefault()); } } - else if (userData is (Order minimapOrder, string option) && minimapOrder.HasOptions && string.IsNullOrEmpty(option)) + else if (userData is MinimapNodeData {Order: { } minimapOrder} && minimapOrder.Prefab.HasOptions) { CreateOrderOptionNodes(minimapOrder, minimapOrder.TargetEntity as Item); } @@ -2383,7 +2387,7 @@ namespace Barotrauma { if (commandFrame != null) { - optionNodes.ForEach(node => commandFrame.RemoveChild(node.Item1)); + optionNodes.ForEach(node => commandFrame.RemoveChild(node.Button)); shortcutNodes.ForEach(node => commandFrame.RemoveChild(node)); commandFrame.RemoveChild(expandNode); } @@ -2421,19 +2425,23 @@ namespace Barotrauma }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); - if (Order.OrderCategoryIcons.TryGetValue(category, out Tuple sprite)) + var icon = OrderCategoryIcon.OrderCategoryIcons.FirstOrDefault(ic => ic.Category == category); + if (!(icon is null)) { - var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLowerInvariant()); - var categoryDescription = TextManager.Get("ordercategorydescription." + category.ToString(), true); - if (!string.IsNullOrWhiteSpace(categoryDescription)) { tooltip += "\n" + categoryDescription; } - CreateNodeIcon(Vector2.One, node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); + var tooltip = TextManager.Get($"ordercategorytitle.{category}"); + var categoryDescription = TextManager.Get($"ordercategorydescription.{category}"); + if (!categoryDescription.IsNullOrWhiteSpace()) { tooltip += "\n" + categoryDescription; } + CreateNodeIcon(Vector2.One, node.RectTransform, icon.Sprite, icon.Color, tooltip: tooltip); } CreateHotkeyIcon(node.RectTransform, hotkey % 10); - optionNodes.Add(new Tuple(node, Keys.D0 + hotkey % 10)); + optionNodes.Add(new OptionNode(node, Keys.D0 + hotkey % 10)); } private void CreateShortcutNodes() { + bool HasAppropriateJobId(Character c, Identifier jobId) => c.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(jobId); + bool HasAppropriateJob(Character c, string jobId) => HasAppropriateJobId(c, jobId.ToIdentifier()); + var sub = GetTargetSubmarine(); if (sub == null) { return; } shortcutNodes.Clear(); @@ -2444,88 +2452,97 @@ namespace Barotrauma // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { - var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); - string option = order.Prefab.Options[0]; - if (IsNonDuplicateOrder(order, option)) + var orderPrefab = OrderPrefab.Prefabs["operatereactor"]; + var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor, Character.Controlled); + if (IsNonDuplicateOrder(order)) { - shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); + shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, -1)); } } } // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order - if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && Order.GetPrefab("steer") is Order steerOrder && IsNonDuplicateOrder(steerOrder) && + if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, steerOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering, Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order if (CanFitMoreNodes() && ShouldDelegateOrder("fightintruders") && - Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders) && - Order.GetPrefab("fightintruders") is Order fightOrder && IsNonDuplicateOrder(fightOrder)) + ActiveOrders.Any(o => o.Order.Identifier == "reportintruders") && + IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fightintruders"])) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fightOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["fightintruders"], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order - if (CanFitMoreNodes() && ShouldDelegateOrder("fixleaks") && Order.GetPrefab("fixleaks") is Order fixLeaksOrder && IsNonDuplicateOrder(fixLeaksOrder) && - Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach)) + if (CanFitMoreNodes() && ShouldDelegateOrder("fixleaks") && + IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fixleaks"]) && + ActiveOrders.Any(o => o.Order.Identifier == "reportbreach")) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fixLeaksOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["fixleaks"], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } // --> Create shortcut nodes for the Repair orders - if (CanFitMoreNodes() && Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices)) + if (CanFitMoreNodes() && ActiveOrders.Any(o => o.Order.Identifier == "reportbrokendevices")) { + var reportBrokenDevices = OrderPrefab.Prefabs["reportbrokendevices"]; // TODO: Doesn't work for player issued reports, because they don't have a target. bool useSpecificRepairOrder = false; string tag = "repairelectrical"; if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && - ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) { - if (Order.GetPrefab(tag) is Order repairElectricalOrder && IsNonDuplicateOrder(repairElectricalOrder)) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairElectricalOrder, -1)); + var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } useSpecificRepairOrder = true; } tag = "repairmechanical"; if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && - ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) { - if (Order.GetPrefab(tag) is Order repairMechanicalOrder && IsNonDuplicateOrder(repairMechanicalOrder)) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairMechanicalOrder, -1)); + var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } useSpecificRepairOrder = true; } tag = "repairsystems"; - if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && Order.GetPrefab(tag) is Order repairOrder && IsNonDuplicateOrder(repairOrder)) + if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && OrderPrefab.Prefabs[tag] is OrderPrefab repairOrder && IsNonDuplicateOrderPrefab(repairOrder)) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairOrder, -1)); + var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } } // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (CanFitMoreNodes() && Order.GetPrefab("extinguishfires") is Order extinguishOrder && IsNonDuplicateOrder(extinguishOrder) && - ActiveOrders.Any(o => o.First.Prefab == Order.GetPrefab("reportfire"))) + if (CanFitMoreNodes() && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["extinguishfires"]) && + ActiveOrders.Any(o => o.Order.Identifier == "reportfire")) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, extinguishOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["extinguishfires"], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } if (CanFitMoreNodes() && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { - foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) + foreach (Identifier orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) { - if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && IsNonDuplicateOrder(orderPrefab) && - shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || - (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && + if (OrderPrefab.Prefabs[orderIdentifier] is OrderPrefab orderPrefab && IsNonDuplicateOrderPrefab(orderPrefab) && + shortcutNodes.None(n => n.UserData is Order order && order.Identifier == orderIdentifier) && !orderPrefab.IsReport && orderPrefab.Category != null) { if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true, interactableFor: characterContext ?? Character.Controlled).Any()) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); + var order = new Order(orderPrefab, null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } if (!CanFitMoreNodes()) { break; } } @@ -2533,7 +2550,9 @@ namespace Barotrauma } if (CanFitMoreNodes() && characterContext != null && !characterContext.IsDismissed) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); + var order = new Order(OrderPrefab.Dismissal, null, orderGiver: Character.Controlled); + shortcutNodes.Add( + CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } shortcutNodes.RemoveAll(n => n.UserData is Order o && !IsOrderAvailable(o)); if (shortcutNodes.Count < 1) { return; } @@ -2562,32 +2581,34 @@ namespace Barotrauma { return shortcutNodes.Count < maxShortcutNodeCount; } - static bool ShouldDelegateOrder(string orderIdentifier) + static bool ShouldDelegateOrder(string orderIdentifier) => ShouldDelegateOrderId(orderIdentifier.ToIdentifier()); + static bool ShouldDelegateOrderId(Identifier orderIdentifier) { return !(Character.Controlled is Character c) || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); } - bool IsNonDuplicateOrder(Order orderPrefab, string option = null) + bool IsNonDuplicateOrder(Order order) => IsNonDuplicateOrderPrefab(order.Prefab, order.Option); + bool IsNonDuplicateOrderPrefab(OrderPrefab orderPrefab, Identifier option = default) { - return characterContext == null || (string.IsNullOrEmpty(option) ? - characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier) : - characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier && oi.OrderOption == option)); + return characterContext == null || (option.IsEmpty ? + characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier) : + characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi.Option == option)); } } private void CreateOrderNodes(OrderCategory orderCategory) { - var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)); + var orderPrefabs = OrderPrefab.Prefabs.Where(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)).ToArray(); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, - GetCircumferencePointCount(orders.Count), GetFirstNodeAngle(orders.Count)); - for (int i = 0; i < orders.Count; i++) + GetCircumferencePointCount(orderPrefabs.Length), GetFirstNodeAngle(orderPrefabs.Length)); + for (int i = 0; i < orderPrefabs.Length; i++) { - order = orders[i]; + order = new Order(orderPrefabs[i], null, orderGiver: Character.Controlled); disableNode = !CanCharacterBeHeard() || - (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) && - order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); - optionNodes.Add(new Tuple( + (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) && + order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); + optionNodes.Add(new OptionNode( CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } @@ -2605,74 +2626,76 @@ namespace Barotrauma if (itemContext != null && itemContext.IsPlayerTeamInteractable) { ItemComponent targetComponent; - foreach (Order p in Order.PrefabList) + foreach (OrderPrefab p in OrderPrefab.Prefabs) { targetComponent = null; if (p.UseController && itemContext.Components.None(c => c is Controller)) { continue; } if (p.HasOptionSpecificTargetItems) { - foreach (string option in p.Options) + foreach (Identifier option in p.Options) { if (p.TargetItemsMatchItem(itemContext, option)) { - contextualOrders.Add(new OrderInfo(new Order(p, itemContext, targetComponent, Character.Controlled), option)); + contextualOrders.Add(new Order(p, itemContext, targetComponent, Character.Controlled).WithOption(option)); } } } else if (p.TargetItemsMatchItem(itemContext) || p.TryGetTargetItemComponent(itemContext, out targetComponent)) { - contextualOrders.Add(new OrderInfo(p.HasOptions ? p : new Order(p, itemContext, targetComponent, Character.Controlled), null)); + contextualOrders.Add(p.HasOptions ? + new Order(p, null, orderGiver: Character.Controlled) : + new Order(p, itemContext, targetComponent, Character.Controlled)); } } // If targeting a periscope connected to a turret, show the 'operateweapons' order orderIdentifier = "operateweapons"; - var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) + var operateWeaponsPrefab = OrderPrefab.Prefabs[orderIdentifier]; + if (contextualOrders.None(o => o.Identifier == orderIdentifier) && itemContext.Components.Any(c => c is Controller)) { var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)); if (turret != null) { - contextualOrders.Add(new OrderInfo(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled), null)); + contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } } // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order orderIdentifier = "repairsystems"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) + if (contextualOrders.None(order => order.Identifier == orderIdentifier) && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) { if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("repairelectrical"), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null, Character.Controlled)); } else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("repairmechanical"), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null, Character.Controlled)); } else { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); } } // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) orderIdentifier = "pumpwater"; - if (contextualOrders.FirstOrDefault(info => info.Order.Identifier.Equals(orderIdentifier)) is OrderInfo pumpOrderInfo && pumpOrderInfo.Order is Order pumpOrder && + if (contextualOrders.FirstOrDefault(order => order.Identifier.Equals(orderIdentifier)) is Order pumpOrder && itemContext.Components.FirstOrDefault(c => c.GetType() == pumpOrder.ItemComponentType) is Pump pump && pump.IsAutoControlled) { - contextualOrders.Remove(pumpOrderInfo); + contextualOrders.Remove(pumpOrder); } orderIdentifier = "cleanupitems"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(info => info.Identifier.Equals(orderIdentifier))) { if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); } } AddIgnoreOrder(itemContext); } else if (hullContext != null) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("fixleaks"), hullContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["fixleaks"], hullContext, targetItem: null, Character.Controlled)); if (wallContext != null) { AddIgnoreOrder(wallContext); @@ -2681,14 +2704,14 @@ namespace Barotrauma void AddIgnoreOrder(IIgnorable target) { var orderIdentifier = "ignorethis"; - if (!target.OrderedToBeIgnored && contextualOrders.None(info => info.Order.Identifier == orderIdentifier)) + if (!target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); } else { orderIdentifier = "unignorethis"; - if (target.OrderedToBeIgnored && contextualOrders.None(info => info.Order.Identifier == orderIdentifier)) + if (target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); } @@ -2698,62 +2721,62 @@ namespace Barotrauma { if (target is WallSection ws) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled)); } else { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), target as Entity, null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], target as Entity, null, Character.Controlled)); } } } orderIdentifier = "wait"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) { Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), new OrderTarget(position, hull), Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], new OrderTarget(position, hull), Character.Controlled)); } - if (contextualOrders.None(info => info.Order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) + if (contextualOrders.None(order => order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) { orderIdentifier = "follow"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) { - contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); } } // Show 'dismiss' order only when there are crew members with active orders orderIdentifier = "dismissed"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) + if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) { - contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); } } - contextualOrders.RemoveAll(o => !IsOrderAvailable(o.Order)); + contextualOrders.RemoveAll(o => !IsOrderAvailable(o)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); bool disableNode = !CanCharacterBeHeard(); for (int i = 0; i < contextualOrders.Count; i++) { - var info = contextualOrders[i]; + var order = contextualOrders[i]; int hotkey = (i + 1) % 10; - var component = string.IsNullOrEmpty(info.OrderOption) ? - CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), info.Order, hotkey, disableNode: disableNode, checkIfOrderCanBeHeard: false) : - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), info.Order, info.OrderOption, info.Order.Prefab.GetOptionName(info.OrderOption), hotkey); - optionNodes.Add(new Tuple(component, !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); + var component = order.Option.IsEmpty ? + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, hotkey, disableNode: disableNode, checkIfOrderCanBeHeard: false) : + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, hotkey); + optionNodes.Add(new OptionNode(component, !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } // TODO: there's duplicate logic here and above -> would be better to refactor so that the conditions are only defined in one place public static bool DoesItemHaveContextualOrders(Item item) { - if (Order.PrefabList.Any(o => o.TargetItemsMatchItem(item))) { return true; } - if (Order.PrefabList.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } + if (OrderPrefab.Prefabs.Any(o => o.TargetItemsMatchItem(item))) { return true; } + if (OrderPrefab.Prefabs.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } if (AIObjectiveCleanupItems.IsValidTarget(item, Character.Controlled, checkInventory: false)) { return true; } if (AIObjectiveCleanupItems.IsValidContainer(item, Character.Controlled)) { return true; } - if (Order.GetPrefab("loaditems") is Order loadItemsPrefab && AIObjectiveLoadItems.IsValidTarget(item, Character.Controlled, targetContainerTags: loadItemsPrefab.GetTargetItems())) { return true; } + if (OrderPrefab.Prefabs.TryGet("loaditems", out OrderPrefab loadItemsPrefab) && AIObjectiveLoadItems.IsValidTarget(item, Character.Controlled, targetContainerTags: loadItemsPrefab.GetTargetItems())) { return true; } if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { return true; } - return Order.GetPrefab("operateweapons") is Order operateWeaponsPrefab && item.Components.Any(c => c is Controller) && - (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) || - item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item))); + return OrderPrefab.Prefabs.TryGet("operateweapons", out OrderPrefab operateWeaponsPrefab) && item.Components.Any(c => c is Controller) && + (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) || + item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item))); } /// Use a negative value (e.g. -1) if there should be no hotkey associated with the node @@ -2772,7 +2795,7 @@ namespace Barotrauma disableNode = !CanCharacterBeHeard(); } - bool mustSetOptionOrTarget = order.HasOptions; + bool mustSetOptionOrTarget = order.Prefab.HasOptions; Item orderTargetEntity = null; // If the order doesn't have options, but must set a target, @@ -2811,7 +2834,7 @@ namespace Barotrauma } var character = !o.TargetAllCharacters ? characterContext ?? GetCharacterForQuickAssignment(o) : null; int priority = GetManualOrderPriority(character, o); - SetCharacterOrder(character, o, null, priority, Character.Controlled); + SetCharacterOrder(character, o.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); DisableCommandUI(); } return true; @@ -2840,6 +2863,11 @@ namespace Barotrauma return node; } + private struct MinimapNodeData + { + public Order Order; + } + private void CreateMinimapNodes(Order order, Submarine submarine, List matchingItems) { // TODO: Further adjustments to frameSize calculations @@ -2899,7 +2927,10 @@ namespace Barotrauma anchor = Anchor.BottomLeft; } - var userData = new Tuple(item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), ""); + var userData = new MinimapNodeData + { + Order = item == null ? order : order.WithItemComponent(item, order.GetTargetItemComponent(item)) + }; var optionElement = new GUIButton( new RectTransform( new Point((int)(50 * GUI.Scale)), @@ -2908,38 +2939,42 @@ namespace Barotrauma style: null) { UserData = userData, - Font = GUI.SmallFont, - OnClicked = (button, userData) => + Font = GUIStyle.SmallFont, + OnClicked = (button, obj) => { if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - if (o.Item1.HasOptions) + var o = (MinimapNodeData)obj; + if (o.Order.Prefab.HasOptions) { - NavigateForward(button, userData); + NavigateForward(button, o); } - else if (o.Item1.MustManuallyAssign && characterContext == null) + else if (o.Order.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(button); } else { - var character = characterContext ?? GetCharacterForQuickAssignment(o.Item1); - int priority = GetManualOrderPriority(character, o.Item1); - SetCharacterOrder(character, o.Item1, o.Item2, priority, Character.Controlled); + var character = characterContext ?? GetCharacterForQuickAssignment(o.Order); + int priority = GetManualOrderPriority(character, o.Order); + SetCharacterOrder( + character, + o.Order + .WithManualPriority(priority) + .WithOrderGiver(Character.Controlled)); DisableCommandUI(); } return true; } }; - if (CanOpenManualAssignment(optionElement)) + if (CanOpenManualAssignmentMinimapOrder(optionElement)) { optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); } - var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && - o.Order.Identifier == userData.Item1.Identifier && - o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; + var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o != null && + o.Identifier == userData.Order.Identifier && + o.TargetEntity == userData.Order.TargetEntity)) ? 0.5f : 1f; CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); - optionNodes.Add(new Tuple(optionElement, Keys.None)); + optionNodes.Add(new OptionNode(optionElement, Keys.None)); optionElements.Add(optionElement); } @@ -2967,37 +3002,37 @@ namespace Barotrauma targetItem = !order.UseController ? itemContext : itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item; } - var o = (targetItem == null || !order.IsPrefab) ? order : new Order(order, targetItem, order.GetTargetItemComponent(targetItem)); + var o = targetItem == null ? order : order.WithItemComponent(targetItem, order.GetTargetItemComponent(targetItem)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(order.Options.Length), GetFirstNodeAngle(order.Options.Length)); var offsetIndex = 0; for (int i = 0; i < order.Options.Length; i++) { - optionNodes.Add(new Tuple( - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), + optionNodes.Add(new OptionNode( + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o.WithOption(order.Options[i]), (i + 1) % 10), Keys.D0 + (i + 1) % 10)); } } - private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, string option, string optionName, int hotkey) + private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, int hotkey) { var node = new GUIButton(new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) { - UserData = new Tuple(order, option), + UserData = order, OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - if (o.Item1.MustManuallyAssign && characterContext == null) + var o = userData as Order; + if (o.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(button); } else { - var character = characterContext ?? GetCharacterForQuickAssignment(o.Item1); - int priority = GetManualOrderPriority(character, o.Item1); - SetCharacterOrder(character, o.Item1, o.Item2, priority, Character.Controlled); + var character = characterContext ?? GetCharacterForQuickAssignment(o); + int priority = GetManualOrderPriority(character, o); + SetCharacterOrder(character, o.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); DisableCommandUI(); } return true; @@ -3010,8 +3045,9 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); GUIImage icon = null; - if (order.Prefab.OptionSprites.TryGetValue(option, out Sprite sprite)) + if (order.Prefab.OptionSprites.TryGetValue(order.Option, out Sprite sprite)) { + var optionName = order.Prefab.GetOptionName(order.Option); var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + @@ -3039,13 +3075,11 @@ namespace Barotrauma return false; } - var order = (node.UserData is Order) ? - new Tuple(node.UserData as Order, null) : - node.UserData as Tuple; - var characters = GetCharactersForManualAssignment(order.Item1); + var order = node.UserData is MinimapNodeData minimapNodeData ? minimapNodeData.Order : node.UserData as Order; + var characters = GetCharactersForManualAssignment(order); if (characters.None()) { return false; } - if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) + if (!(optionNodes.Find(n => n.Button == node) is OptionNode optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; @@ -3065,7 +3099,7 @@ namespace Barotrauma } else { - if (string.IsNullOrEmpty(order.Item2)) + if (order.Option.IsEmpty) { SetCenterNode(node as GUIButton, resetAnchor: true); } @@ -3077,9 +3111,9 @@ namespace Barotrauma { UserData = node.UserData }; - if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) + if (order.Prefab.OptionSprites.TryGetValue(order.Option, out Sprite sprite)) { - CreateNodeIcon(Vector2.One, clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + CreateNodeIcon(Vector2.One, clickedOptionNode.RectTransform, sprite, order.Color, tooltip: order.GetOptionName(order.Option)); //TODO: revise tooltip } SetCenterNode(clickedOptionNode); node = null; @@ -3146,7 +3180,7 @@ namespace Barotrauma UserData = order, OnClicked = ExpandAssignmentNodes }; - CreateNodeIcon(expandNode.RectTransform, "CommandExpandNode", order.Item1.Color, tooltip: TextManager.Get("commandui.expand")); + CreateNodeIcon(expandNode.RectTransform, "CommandExpandNode", order.Color, tooltip: TextManager.Get("commandui.expand")); hotkey = optionNodes.Count + 1; CreateHotkeyIcon(expandNode.RectTransform, hotkey % 10); @@ -3186,12 +3220,12 @@ namespace Barotrauma firstAngle: MathHelper.ToRadians(-90f - ((extraOptionCharacters.Count - 1) * 0.5f * (360f / availableNodePositions)))); for (int i = 0; i < extraOptionCharacters.Count && i < availableNodePositions; i++) { - CreateAssignmentNode(userData as Tuple, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); + CreateAssignmentNode(userData as Order, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); } return true; } - private void CreateAssignmentNode(Tuple order, Character character, Point offset, int hotkey, float nameLabelScale = 1f) + private void CreateAssignmentNode(Order order, Character character, Point offset, int hotkey, float nameLabelScale = 1f) { // Button var node = new GUIButton( @@ -3203,8 +3237,8 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var character = userData as Character; - int priority = GetManualOrderPriority(character, order.Item1); - SetCharacterOrder(character, order.Item1, order.Item2, priority, Character.Controlled); + int priority = GetManualOrderPriority(character, order); + SetCharacterOrder(character, order.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); DisableCommandUI(); return true; } @@ -3216,11 +3250,11 @@ namespace Barotrauma // Order icon var topOrderInfo = character.GetCurrentOrderWithTopPriority(); GUIImage orderIcon; - if (topOrderInfo.HasValue) + if (topOrderInfo != null) { - orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), topOrderInfo.Value.Order.SymbolSprite, scaleToFit: true); - var tooltip = topOrderInfo.Value.Order.Name; - if (!string.IsNullOrWhiteSpace(topOrderInfo.Value.OrderOption)) { tooltip += " (" + topOrderInfo.Value.Order.GetOptionName(topOrderInfo.Value.OrderOption) + ")"; }; + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), topOrderInfo.SymbolSprite, scaleToFit: true); + var tooltip = topOrderInfo.Name; + if (topOrderInfo.Option != Identifier.Empty) { tooltip += " (" + topOrderInfo.GetOptionName(topOrderInfo.Option) + ")"; }; orderIcon.ToolTip = tooltip; } else @@ -3235,7 +3269,7 @@ namespace Barotrauma // Name label var width = (int)(nameLabelScale * nodeSize.X); - var font = GUI.SmallFont; + var font = GUIStyle.SmallFont; var nameLabel = new GUITextBlock( new RectTransform(new Point(width, 0), parent: node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.BottomCenter) { @@ -3244,7 +3278,7 @@ namespace Barotrauma ToolBox.LimitString(character.Info?.DisplayName, font, width), textColor: jobColor * nodeColorMultiplier, font: font, textAlignment: Alignment.Center, style: null) { CanBeFocused = false, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, HoverTextColor = jobColor }; @@ -3277,7 +3311,7 @@ namespace Barotrauma if (hotkey >= 0) { if (canHear) { CreateHotkeyIcon(node.RectTransform, hotkey); } - optionNodes.Add(new Tuple(node, canHear ? Keys.D0 + hotkey : Keys.None)); + optionNodes.Add(new OptionNode(node, canHear ? Keys.D0 + hotkey : Keys.None)); } else { @@ -3285,7 +3319,7 @@ namespace Barotrauma } } - private GUIImage CreateNodeIcon(Vector2 relativeSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) + private GUIImage CreateNodeIcon(Vector2 relativeSize, RectTransform parent, Sprite sprite, Color color, LocalizedString tooltip = null) { // Icon return new GUIImage( @@ -3305,7 +3339,7 @@ namespace Barotrauma /// /// Create node icon with a fixed absolute size /// - private GUIImage CreateNodeIcon(Point absoluteSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) + private GUIImage CreateNodeIcon(Point absoluteSize, RectTransform parent, Sprite sprite, Color color, LocalizedString tooltip = null) { // Icon return new GUIImage( @@ -3322,7 +3356,7 @@ namespace Barotrauma }; } - private void CreateNodeIcon(RectTransform parent, string style, Color? color = null, string tooltip = null) + private void CreateNodeIcon(RectTransform parent, string style, Color? color = null, LocalizedString tooltip = null) { // Icon var icon = new GUIImage( @@ -3364,21 +3398,19 @@ namespace Barotrauma }; } - private void CreateBlockIcon(RectTransform parent, string tooltip = null) + private void CreateBlockIcon(RectTransform parent, LocalizedString tooltip = null) { var icon = new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) { CanBeFocused = false, - Color = GUI.Style.Red * nodeColorMultiplier, - HoverColor = GUI.Style.Red + Color = GUIStyle.Red * nodeColorMultiplier, + HoverColor = GUIStyle.Red }; - if (!string.IsNullOrEmpty(tooltip)) + if (!tooltip.IsNullOrEmpty()) { - icon.ToolTip = tooltip; - string color = XMLExtensions.ColorToString(GUI.Style.Red); + string color = XMLExtensions.ColorToString(GUIStyle.Red); tooltip = $"‖color:{color}‖{tooltip}‖color:end‖"; - var richTextData = RichTextData.GetRichTextData(tooltip, out _); - icon.TooltipRichTextData = richTextData; + icon.ToolTip = RichString.Rich(tooltip); icon.CanBeFocused = true; } } @@ -3469,13 +3501,13 @@ namespace Barotrauma private void SetCharacterTooltip(GUIComponent component, Character character) { if (component == null) { return; } - var tooltip = character?.Info != null ? characterContext.Info.DisplayName : null; - if (string.IsNullOrWhiteSpace(tooltip)) { component.ToolTip = tooltip; return; } - if (character.Info?.Job != null && !string.IsNullOrWhiteSpace(characterContext.Info.Job.Name)) { tooltip += " (" + characterContext.Info.Job.Name + ")"; } + LocalizedString tooltip = character?.Info != null ? characterContext.Info.DisplayName : null; + if (tooltip.IsNullOrWhiteSpace()) { component.ToolTip = tooltip; return; } + if (character.Info?.Job != null && !characterContext.Info.Job.Name.IsNullOrWhiteSpace()) { tooltip += " (" + characterContext.Info.Job.Name + ")"; } component.ToolTip = tooltip; } - private string GetOrderNameBasedOnContextuality(Order order) + private LocalizedString GetOrderNameBasedOnContextuality(Order order) { if (order == null) { return ""; } if (isContextual) { return order.ContextualName; } @@ -3488,9 +3520,12 @@ namespace Barotrauma } private bool IsOrderAvailable(Order order) + => IsOrderAvailable(order.Prefab); + + private bool IsOrderAvailable(OrderPrefab order) { if (order == null) { return false; } - switch (order.Identifier.ToLowerInvariant()) + switch (order.Identifier.Value.ToLowerInvariant()) { case "assaultenemy": Character character = characterContext ?? Character.Controlled; @@ -3502,16 +3537,28 @@ namespace Barotrauma } #region Crew Member Assignment Logic - private bool CanOpenManualAssignment(GUIComponent node) + private bool CanOpenManualAssignmentMinimapOrder(GUIComponent node) { if (node == null || characterContext != null) { return false; } - if (node.UserData is (Order minimapOrder, string option)) + if (node.UserData is MinimapNodeData {Order: { } minimapOrder}) { - return !minimapOrder.TargetAllCharacters && (!minimapOrder.HasOptions || !string.IsNullOrEmpty(option)); + return !minimapOrder.TargetAllCharacters && (!minimapOrder.Prefab.HasOptions || !minimapOrder.Option.IsEmpty); } if (node.UserData is Order nodeOrder) { - return !nodeOrder.TargetAllCharacters && !nodeOrder.HasOptions && + return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions && + (!nodeOrder.MustSetTarget || itemContext != null || + nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); + } + return false; + } + + private bool CanOpenManualAssignment(GUIComponent node) + { + if (node == null || characterContext != null) { return false; } + if (node.UserData is Order nodeOrder) + { + return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions && (!nodeOrder.MustSetTarget || itemContext != null || nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); } @@ -3594,11 +3641,15 @@ namespace Barotrauma ReportButtonFrame.Visible = false; } } - + private void ToggleReportButton(string orderIdentifier, bool enabled) { - Order order = Order.GetPrefab(orderIdentifier); - var reportButton = ReportButtonFrame.GetChildByUserData(order); + ToggleReportButton(orderIdentifier.ToIdentifier(), enabled); + } + + private void ToggleReportButton(Identifier orderIdentifier, bool enabled) + { + var reportButton = ReportButtonFrame.FindChild(c => c.UserData is OrderPrefab orderPrefab && orderPrefab.Identifier == orderIdentifier); if (reportButton != null) { reportButton.GetChildByUserData("highlighted").Visible = enabled; @@ -3651,12 +3702,12 @@ namespace Barotrauma ushort orderGiverId = inc.ReadUInt16(); orderGiver = orderGiverId != Entity.NullEntityID ? Entity.FindEntityByID(orderGiverId) as Character : null; } - if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) + if (orderMessageInfo.OrderIdentifier == Identifier.Empty) { - DebugConsole.ThrowError("Invalid active order - order index out of bounds."); + DebugConsole.ThrowError("Invalid active order - order identifier empty."); continue; } - Order orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; + OrderPrefab orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; Order order = orderMessageInfo.TargetType switch { Order.OrderTargetType.Entity => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 6fa04747c..9f8368a3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -14,7 +14,7 @@ namespace Barotrauma protected bool crewDead; protected Color overlayColor; - protected string overlayText, overlayTextBottom; + protected LocalizedString overlayText, overlayTextBottom; protected Color overlayTextColor; protected Sprite overlaySprite; @@ -68,8 +68,8 @@ namespace Barotrauma foreach (Mission mission in Missions.ToList()) { new GUIMessageBox( - mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name, - mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon, parseRichText: true) + RichString.Rich(mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name), + RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, UserData = "missionstartmessage" @@ -123,12 +123,12 @@ namespace Barotrauma { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); } - if (!string.IsNullOrEmpty(overlayText) && overlayTextColor.A > 0) + if (!overlayText.IsNullOrEmpty() && overlayTextColor.A > 0) { - var backgroundSprite = GUI.Style.GetComponentStyle("CommandBackground").GetDefaultSprite(); + var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; - string wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUI.Font); - Vector2 textSize = GUI.Font.MeasureString(wrappedText); + LocalizedString wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUIStyle.Font); + Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); Vector2 textPos = centerPos - textSize / 2; backgroundSprite.Draw(spriteBatch, centerPos, @@ -140,11 +140,11 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); - if (!string.IsNullOrEmpty(overlayTextBottom)) + if (!overlayTextBottom.IsNullOrEmpty()) { - Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUI.Font.MeasureString(overlayTextBottom) / 2; - GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom, overlayTextColor); + Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(overlayTextBottom) / 2; + GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom.Value, Color.Black * (overlayTextColor.A / 255.0f)); + GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom.Value, overlayTextColor); } } } @@ -159,7 +159,7 @@ namespace Barotrauma endRoundButton.Visible = false; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); - string buttonText = ""; + LocalizedString buttonText = ""; switch (availableTransition) { case TransitionType.ProgressToNextLocation: @@ -188,7 +188,7 @@ namespace Barotrauma case TransitionType.None: default: if (Level.Loaded.Type == LevelData.LevelType.Outpost && - (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false))) + (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; @@ -218,7 +218,7 @@ namespace Barotrauma endRoundButton.OnClicked(EndRoundButton, null); prevCampaignUIAutoOpenType = availableTransition; } - endRoundButton.Text = ToolBox.LimitString(buttonText, endRoundButton.Font, endRoundButton.Rect.Width - 5); + endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5); if (endRoundButton.Text != buttonText) { endRoundButton.ToolTip = buttonText; @@ -244,7 +244,7 @@ namespace Barotrauma if (ReadyCheck.ReadyCheckCooldown > DateTime.Now) { float progress = (ReadyCheck.ReadyCheckCooldown - DateTime.Now).Seconds / 60.0f; - ReadyCheckButton.Color = ToolBox.GradientLerp(progress, Color.White, GUI.Style.Red); + ReadyCheckButton.Color = ToolBox.GradientLerp(progress, Color.White, GUIStyle.Red); } } } @@ -289,7 +289,7 @@ namespace Barotrauma case InteractionType.Examine: return; case InteractionType.Upgrade when !UpgradeManager.CanUpgradeSub(): - UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade"), IsSinglePlayer, npc); + UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade").Value, IsSinglePlayer, npc); return; case InteractionType.Crew when GameMain.NetworkMember != null: CampaignUI.CrewManagement.SendCrewState(false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs index 143f88789..8e0430760 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -52,39 +53,39 @@ namespace Barotrauma text = text.TrimEnd('\n'); - List richTextDatas = RichTextData.GetRichTextData(text, out text) ?? new List(); + ImmutableArray? richTextDatas = RichTextData.GetRichTextData(text, out text); - Vector2 size = GUI.SmallFont.MeasureString(text); + Vector2 size = GUIStyle.SmallFont.MeasureString(text); Vector2 infoPos = new Vector2(GameMain.GraphicsWidth - size.X - 16, pos.Y + 8); Rectangle infoRect = new Rectangle(infoPos.ToPoint(), size.ToPoint()); infoRect.Inflate(8, 8); GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, infoRect, Color.White * 0.8f); - if (richTextDatas.Any()) + if (richTextDatas != null && richTextDatas.Value.Any()) { - GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextDatas, font: GUI.SmallFont); + GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextDatas.Value, font: GUIStyle.SmallFont); } else { - GUI.DrawString(spriteBatch, infoPos, text, Color.White, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, infoPos, text, Color.White, font: GUIStyle.SmallFont); } float y = infoRect.Bottom + 16; if (Campaign.Factions != null) { const string factionHeader = "Reputations"; - Vector2 factionHeaderSize = GUI.SubHeadingFont.MeasureString(factionHeader); + Vector2 factionHeaderSize = GUIStyle.SubHeadingFont.MeasureString(factionHeader); Vector2 factionPos = new Vector2(GameMain.GraphicsWidth - (264 / 2) - factionHeaderSize.X / 2, y); - GUI.DrawString(spriteBatch, factionPos, factionHeader, Color.White, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, factionPos, factionHeader, Color.White, font: GUIStyle.SubHeadingFont); y += factionHeaderSize.Y + 8; foreach (Faction faction in Campaign.Factions) { - string name = faction.Prefab.Name; - Vector2 nameSize = GUI.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUI.SmallFont); + LocalizedString name = faction.Prefab.Name; + Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUIStyle.SmallFont); y += nameSize.Y + 5; Color color = ToolBox.GradientLerp(faction.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); @@ -98,8 +99,8 @@ namespace Barotrauma if (location?.Reputation != null) { string name = Campaign.Map?.CurrentLocation.Name; - Vector2 nameSize = GUI.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUI.SmallFont); + Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUIStyle.SmallFont); y += nameSize.Y + 5; float normalizedReputation = MathUtils.InverseLerp(location.Reputation.MinReputation, location.Reputation.MaxReputation, location.Reputation.Value); @@ -107,8 +108,6 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, (int)(normalizedReputation * 255), 10), color, isFilled: true); GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, 256, 10), Color.White); } - - richTextDatas.Clear(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 167fab3e8..d628ec7fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -217,8 +217,7 @@ namespace Barotrauma overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); overlayTextColor = Color.Transparent; overlayText = TextManager.GetWithVariables("campaignstart", - new string[] { "xxxx", "yyyy" }, - new string[] { Map.CurrentLocation.Name, TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass) }); + ("xxxx", Map.CurrentLocation.Name), ("yyyy", TextManager.Get($"submarineclass.{Submarine.MainSub.Info.SubmarineClass}"))); float fadeInDuration = 1.0f; float textDuration = 10.0f; float timer = 0.0f; @@ -317,7 +316,7 @@ namespace Barotrauma private IEnumerable DoLevelTransition() { - SoundPlayer.OverrideMusicType = CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead"; + SoundPlayer.OverrideMusicType = (CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; Level prevLevel = Level.Loaded; @@ -584,7 +583,7 @@ namespace Barotrauma foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) { msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? string.Empty); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); } } @@ -610,11 +609,11 @@ namespace Barotrauma float? reputation = null; if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } - Dictionary factionReps = new Dictionary(); + Dictionary factionReps = new Dictionary(); byte factionsCount = msg.ReadByte(); for (int i = 0; i < factionsCount; i++) { - factionReps.Add(msg.ReadString(), msg.ReadSingle()); + factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); } bool forceMapUI = msg.ReadBoolean(); @@ -625,12 +624,12 @@ namespace Barotrauma bool purchasedLostShuttles = msg.ReadBoolean(); byte missionCount = msg.ReadByte(); - List> availableMissions = new List>(); + var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); for (int i = 0; i < missionCount; i++) { - string missionIdentifier = msg.ReadString(); + Identifier missionIdentifier = msg.ReadIdentifier(); byte connectionIndex = msg.ReadByte(); - availableMissions.Add(new Pair(missionIdentifier, connectionIndex)); + availableMissions.Add((missionIdentifier, connectionIndex)); } UInt16? storeBalance = null; @@ -643,7 +642,7 @@ namespace Barotrauma List buyCrateItems = new List(); for (int i = 0; i < buyCrateItemCount; i++) { - string itemPrefabIdentifier = msg.ReadString(); + Identifier itemPrefabIdentifier = msg.ReadIdentifier(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } @@ -661,7 +660,7 @@ namespace Barotrauma List purchasedItems = new List(); for (int i = 0; i < purchasedItemCount; i++) { - string itemPrefabIdentifier = msg.ReadString(); + Identifier itemPrefabIdentifier = msg.ReadIdentifier(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } @@ -670,7 +669,7 @@ namespace Barotrauma List soldItems = new List(); for (int i = 0; i < soldItemCount; i++) { - string itemPrefabIdentifier = msg.ReadString(); + Identifier itemPrefabIdentifier = msg.ReadIdentifier(); UInt16 id = msg.ReadUInt16(); bool removed = msg.ReadBoolean(); byte sellerId = msg.ReadByte(); @@ -682,9 +681,9 @@ namespace Barotrauma List pendingUpgrades = new List(); for (int i = 0; i < pendingUpgradeCount; i++) { - string upgradeIdentifier = msg.ReadString(); + Identifier upgradeIdentifier = msg.ReadIdentifier(); UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); - string categoryIdentifier = msg.ReadString(); + Identifier categoryIdentifier = msg.ReadIdentifier(); UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); int upgradeLevel = msg.ReadByte(); if (prefab == null || category == null) { continue; } @@ -696,8 +695,8 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - string itemToInstallIdentifier = msg.ReadString(); - ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + Identifier itemToInstallIdentifier = msg.ReadIdentifier(); + ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } @@ -769,7 +768,7 @@ namespace Barotrauma foreach (var (identifier, rep) in factionReps) { - Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); if (faction?.Reputation != null) { faction.Reputation.SetReputation(rep); @@ -788,24 +787,24 @@ namespace Barotrauma foreach (var availableMission in availableMissions) { - MissionPrefab missionPrefab = MissionPrefab.List.Find(mp => mp.Identifier == availableMission.First); + MissionPrefab missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == availableMission.Identifier); if (missionPrefab == null) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.First}\" not found."); + DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.Identifier}\" not found."); continue; } - if (availableMission.Second == 255) + if (availableMission.ConnectionIndex == 255) { campaign.Map.CurrentLocation.UnlockMission(missionPrefab); } else { - if (availableMission.Second < 0 || availableMission.Second >= campaign.Map.CurrentLocation.Connections.Count) + if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.First}\" out of range (index: {availableMission.Second}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); + DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); continue; } - LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.Second]; + LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex]; campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } } @@ -849,7 +848,7 @@ namespace Barotrauma List availableHires = new List(); for (int i = 0; i < availableHireLength; i++) { - CharacterInfo hire = CharacterInfo.ClientRead("human", msg); + CharacterInfo hire = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); hire.Salary = msg.ReadInt32(); availableHires.Add(hire); } @@ -865,7 +864,7 @@ namespace Barotrauma List hiredCharacters = new List(); for (int i = 0; i < hiredLength; i++) { - CharacterInfo hired = CharacterInfo.ClientRead("human", msg); + CharacterInfo hired = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); hired.Salary = msg.ReadInt32(); hiredCharacters.Add(hired); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 1cc228e61..fb1181c12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -68,7 +68,7 @@ namespace Barotrauma for (int i = 0; i < jobPrefab.InitialCount; i++) { var variant = Rand.Range(0, jobPrefab.Variants); - CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant)); + CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } InitCampaignData(); @@ -82,7 +82,7 @@ namespace Barotrauma { IsFirstRound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -283,9 +283,9 @@ namespace Barotrauma overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); overlayTextColor = Color.Transparent; overlayText = TextManager.GetWithVariables(showCampaignResetText ? "campaignend4" : "campaignstart", - new string[] { "xxxx", "yyyy" }, - new string[] { Map.CurrentLocation.Name, TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass) }); - string pressAnyKeyText = TextManager.Get("pressanykey"); + ("xxxx", Map.CurrentLocation.Name), + ("yyyy", TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass))); + LocalizedString pressAnyKeyText = TextManager.Get("pressanykey"); float fadeInDuration = 2.0f; float textDuration = 10.0f; float timer = 0.0f; @@ -385,7 +385,7 @@ namespace Barotrauma { NextLevel = newLevel; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); - SoundPlayer.OverrideMusicType = success ? "endround" : "crewdead"; + SoundPlayer.OverrideMusicType = (success ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; GUI.SetSavingIndicatorState(success); crewDead = false; @@ -672,9 +672,9 @@ namespace Barotrauma var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); if (subsToLeaveBehind.Any()) { - string msg = TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"); + LocalizedString msg = TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"); - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), msg, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), msg, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked += (btn, userdata) => { LoadNewLevel(); return true; } ; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[0].UserData = Submarine.Loaded.FindAll(s => !subsToLeaveBehind.Contains(s)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 2e4910a85..2cdcec576 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -29,7 +29,7 @@ namespace Barotrauma for (int i = 0; i < jobPrefab.InitialCount; i++) { var variant = Rand.Range(0, jobPrefab.Variants); - CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant)); + CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } } @@ -93,7 +93,7 @@ namespace Barotrauma private void GenerateOutpost(Submarine submarine) { - Submarine outpost = OutpostGenerator.Generate(OutpostParams ?? OutpostGenerationParams.Params.GetRandom(), OutpostType ?? LocationType.List.GetRandom()); + Submarine outpost = OutpostGenerator.Generate(OutpostParams ?? OutpostGenerationParams.OutpostParams.GetRandomUnsynced(), OutpostType ?? LocationType.Prefabs.GetRandomUnsynced()); outpost.SetPosition(Vector2.Zero); float closestDistance = 0.0f; @@ -131,7 +131,7 @@ namespace Barotrauma if (Character.Controlled != null) { - Character.Controlled.TeleportTo(outpost.GetWaypoints(false).GetRandom(point => point.SpawnType == SpawnType.Human).WorldPosition); + Character.Controlled.TeleportTo(outpost.GetWaypoints(false).GetRandomUnsynced(point => point.SpawnType == SpawnType.Human).WorldPosition); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs deleted file mode 100644 index 4431dbabb..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ /dev/null @@ -1,683 +0,0 @@ -using Barotrauma.Items.Components; -using FarseerPhysics; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Tutorials -{ - class BasicTutorial : ScenarioTutorial - { - public BasicTutorial(XElement element) - : base(element) - { - } - - public override IEnumerable UpdateState() - { - Character Controlled = Character.Controlled; - if (Controlled == null) yield return CoroutineStatus.Success; - - foreach (Item item in Item.ItemList) - { - var wire = item.GetComponent(); - if (wire != null && wire.Connections.Any(c => c != null)) - { - wire.Locked = true; - } - } - - //remove all characters except the controlled one to prevent any unintended monster attacks - var existingCharacters = Character.CharacterList.FindAll(c => c != Controlled); - foreach (Character c in existingCharacters) - { - c.Remove(); - } - - yield return new WaitForSeconds(4.0f); - - infoBox = CreateInfoFrame("", "Use WASD to move and the mouse to look around"); - - yield return new WaitForSeconds(5.0f); - - //----------------------------------- - - infoBox = CreateInfoFrame("", "Open the door at your right side by highlighting the button next to it with your cursor and pressing E"); - - Door tutorialDoor = Item.ItemList.Find(i => i.HasTag("tutorialdoor")).GetComponent(); - - while (!tutorialDoor.IsOpen && Controlled.WorldPosition.X < tutorialDoor.Item.WorldPosition.X) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - yield return new WaitForSeconds(2.0f); - - //----------------------------------- - - infoBox = CreateInfoFrame("", "Hold W or S to walk up or down stairs. Use shift to run.", hasButton: true); - - while (infoBox != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - //----------------------------------- - - infoBox = CreateInfoFrame("", "At the moment the submarine has no power, which means that crucial systems such as the oxygen generator or the engine aren't running. Let's fix this: go to the upper left corner of the submarine, where you'll find a nuclear reactor."); - - Reactor reactor = Item.ItemList.Find(i => i.HasTag("tutorialreactor")).GetComponent(); - //reactor.MeltDownTemp = 20000.0f; - - while (Vector2.Distance(Controlled.Position, reactor.Item.Position) > 200.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The reactor requires fuel rods to generate power. You can grab one from the steel cabinet by walking next to it and pressing E."); - - while (Controlled.SelectedConstruction == null || Controlled.SelectedConstruction.Prefab.Identifier != "steelcabinet") - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Pick up one of the fuel rods either by double-clicking or dragging and dropping it into your inventory."); - - while (!HasItem("fuelrod")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Select the reactor by walking next to it and pressing E."); - - while (Controlled.SelectedConstruction != reactor.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "Load the fuel rod into the reactor by dropping it into any of the 5 slots."); - - while (reactor.AvailableFuel <= 0.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The reactor is now fueled up. Try turning it on by increasing the fission rate."); - - while (reactor.FissionRate <= 0.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "The reactor core has started generating heat, which in turn generates power for the submarine. The power generation is very low at the moment," - + " because the reactor is set to shut itself down when the temperature rises above 500 degrees Celsius. You can adjust the temperature limit by changing the \"Shutdown Temperature\" in the control panel.", hasButton: true); - - //TODO: reimplement - /*while (infoBox != null) - { - reactor.ShutDownTemp = Math.Min(reactor.ShutDownTemp, 5000.0f); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("The amount of power generated by the reactor should be kept close to the amount of power consumed by the devices in the submarine. " - + "If there isn't enough power, devices won't function properly (or at all), and if there's too much power, some devices may be damaged." - + " Try to raise the temperature of the reactor close to 3000 degrees by adjusting the fission and cooling rates.", true); - - while (Math.Abs(reactor.Temperature - 3000.0f) > 100.0f) - { - reactor.AutoTemp = false; - reactor.ShutDownTemp = Math.Min(reactor.ShutDownTemp, 5000.0f); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("Looks like we're up and running! Now you should turn on the \"Automatic temperature control\", which will make the reactor " - + "automatically adjust the temperature to a suitable level. Even though it's an easy way to keep the reactor up and running most of the time, " - + "you should keep in mind that it changes the temperature very slowly and carefully, which may cause issues if there are sudden changes in grid load."); - - while (!reactor.AutoTemp) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - }*/ - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "That's the basics of operating the reactor! Now that there's power available for the engines, it's time to get the submarine moving. " - + "Deselect the reactor by pressing E and head to the command room at the right edge of the vessel."); - - Steering steering = Item.ItemList.Find(i => i.HasTag("tutorialsteering")).GetComponent(); - Sonar sonar = steering.Item.GetComponent(); - - while (Vector2.Distance(Controlled.Position, steering.Item.Position) > 150.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - CoroutineManager.StartCoroutine(KeepReactorRunning(reactor)); - - infoBox = CreateInfoFrame("", "Select the navigation terminal by walking next to it and pressing E."); - - while (Controlled.SelectedConstruction != steering.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "There seems to be something wrong with the navigation terminal." + - " There's nothing on the monitor, so it's probably out of power. The reactor must still be" - + " running or the lights would've gone out, so it's most likely a problem with the wiring." - + " Deselect the terminal by pressing E to start checking the wiring."); - - while (Controlled.SelectedConstruction == steering.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(1.0f); - - infoBox = CreateInfoFrame("", "You need a screwdriver to check the wiring of the terminal." - + " Equip a screwdriver by pulling it to either of the slots with a hand symbol, and then use it on the terminal by left clicking."); - - while (Controlled.SelectedConstruction != steering.Item || - Controlled.HeldItems.FirstOrDefault(i => i.Prefab.Identifier == "screwdriver") == null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - - infoBox = CreateInfoFrame("", "Here you can see all the wires connected to the terminal. Apparently there's no wire" - + " going into the to the power connection - that's why the monitor isn't working." - + " You should find a piece of wire to connect it. Try searching some of the cabinets scattered around the sub."); - - while (!HasItem("wire")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Head back to the navigation terminal to fix the wiring."); - - PowerTransfer junctionBox = Item.ItemList.Find(i => i != null && i.HasTag("tutorialjunctionbox")).GetComponent(); - - while ((Controlled.SelectedConstruction != junctionBox.Item && - Controlled.SelectedConstruction != steering.Item) || - !Controlled.HeldItems.Any(i => i.Prefab.Identifier == "screwdriver")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - if (!Controlled.HeldItems.Any(i => i.GetComponent() != null)) - { - infoBox = CreateInfoFrame("", "Equip the wire by dragging it to one of the slots with a hand symbol."); - - while (!Controlled.HeldItems.Any(i => i.GetComponent() != null)) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - } - - infoBox = CreateInfoFrame("", "You can see the equipped wire at the middle of the connection panel. Drag it to the power connector."); - - var steeringConnection = steering.Item.Connections.Find(c => c.Name.Contains("power")); - - while (steeringConnection.Wires.FirstOrDefault(w => w != null) == null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - - } - - infoBox = CreateInfoFrame("", "Now you have to connect the other end of the wire to a power source. " - + "The junction box in the room just below the command room should do."); - - while (Controlled.SelectedConstruction != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - yield return new WaitForSeconds(2.0f); - - infoBox = CreateInfoFrame("", "You can now move the other end of the wire around, and attach it on the wall by left clicking or " - + "remove the previous attachment by right clicking. Or if you don't care for neatly laid out wiring, you can just " - + "run it straight to the junction box."); - - while (Controlled.SelectedConstruction == null || Controlled.SelectedConstruction.GetComponent() == null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Connect the wire to the junction box by pulling it to the power connection, the same way you did with the navigation terminal."); - - while (sonar.Voltage < 0.1f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Great! Now we should be able to get moving."); - - - while (Controlled.SelectedConstruction != steering.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "You can take a look at the area around the sub by selecting the \"Active Sonar\" checkbox."); - - while (!sonar.IsActive) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "The blue rectangle in the middle is the submarine, and the flickering shapes outside it are the walls of an underwater cavern. " - + "Try moving the submarine by clicking somewhere on the monitor and dragging the pointer to the direction you want to go to."); - - while (steering.TargetVelocity == Vector2.Zero && steering.TargetVelocity.Length() < 50.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(4.0f); - - infoBox = CreateInfoFrame("", "The submarine moves up and down by pumping water in and out of the two ballast tanks at the bottom of the submarine. " - + "The engine at the back of the sub moves it forwards and backwards.", hasButton: true); - - while (infoBox != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Steer the submarine downwards, heading further into the cavern."); - - while (Submarine.MainSub.WorldPosition.Y > 32000.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(1.0f); - - var moloch = Character.Create("moloch", steering.Item.WorldPosition + new Vector2(3000.0f, -500.0f), ""); - - moloch.PlaySound(CharacterSound.SoundType.Attack); - - yield return new WaitForSeconds(1.0f); - - infoBox = CreateInfoFrame("", "Uh-oh... Something enormous just appeared on the sonar."); - - List windows = new List(); - foreach (Structure s in Structure.WallList) - { - if (s.CastShadow || !s.HasBody) continue; - - if (s.Rect.Right > steering.Item.CurrentHull.Rect.Right) windows.Add(s); - } - - float slowdownTimer = 1.0f; - bool broken = false; - do - { - steering.TargetVelocity = Vector2.Zero; - - slowdownTimer = Math.Max(0.0f, slowdownTimer - CoroutineManager.DeltaTime * 0.3f); - Submarine.MainSub.Velocity *= slowdownTimer; - - moloch.AIController.SelectTarget(steering.Item.CurrentHull.AiTarget); - Vector2 steeringDir = windows[0].WorldPosition - moloch.WorldPosition; - if (steeringDir != Vector2.Zero) steeringDir = Vector2.Normalize(steeringDir); - - moloch.AIController.SteeringManager.SteeringManual(CoroutineManager.DeltaTime, steeringDir * 100.0f); - - foreach (Structure window in windows) - { - for (int i = 0; i < window.SectionCount; i++) - { - if (!window.SectionIsLeaking(i)) continue; - broken = true; - break; - } - if (broken) break; - } - if (broken) break; - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } while (!broken); - - //fix everything except the command windows - foreach (Structure w in Structure.WallList) - { - bool isWindow = windows.Contains(w); - - for (int i = 0; i < w.SectionCount; i++) - { - if (!w.SectionIsLeaking(i)) continue; - - if (isWindow) - { - //decrease window damage to slow down the leaking - w.AddDamage(i, -w.SectionDamage(i) * 0.48f); - } - else - { - w.AddDamage(i, -100000.0f); - } - } - } - - Submarine.MainSub.GodMode = true; - - var capacitor1 = Item.ItemList.Find(i => i.HasTag("capacitor1")).GetComponent(); - var capacitor2 = Item.ItemList.Find(i => i.HasTag("capacitor1")).GetComponent(); - CoroutineManager.StartCoroutine(KeepEnemyAway(moloch, new PowerContainer[] { capacitor1, capacitor2 })); - - infoBox = CreateInfoFrame("", "The hull has been breached! Close all the doors to the command room to stop the water from flooding the entire sub!"); - - Door commandDoor1 = Item.ItemList.Find(i => i.HasTag("commanddoor1")).GetComponent(); - Door commandDoor2 = Item.ItemList.Find(i => i.HasTag("commanddoor2")).GetComponent(); - - //wait until the player is out of the room and the doors are closed - while (Controlled.WorldPosition.X > commandDoor1.Item.WorldPosition.X || - (commandDoor1.IsOpen || commandDoor2.IsOpen)) - { - //prevent the hull from filling up completely and crushing the player - steering.Item.CurrentHull.WaterVolume = Math.Min(steering.Item.CurrentHull.WaterVolume, steering.Item.CurrentHull.Volume * 0.9f); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - - infoBox = CreateInfoFrame("", "You should quickly find yourself a diving mask or a diving suit. " + - "There are some in the room next to the airlock."); - - bool divingMaskSelected = false; - - while (!HasItem("divingmask") && !HasItem("divingsuit")) - { - if (!divingMaskSelected && - Controlled.FocusedItem != null && Controlled.FocusedItem.Prefab.Identifier == "divingsuit") - { - infoBox = CreateInfoFrame("", "There can only be one item in each inventory slot, so you need to take off " - + "the jumpsuit if you wish to wear a diving suit."); - - divingMaskSelected = true; - } - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - if (HasItem("divingmask")) - { - infoBox = CreateInfoFrame("", "The diving mask will let you breathe underwater, but it won't protect from the water pressure outside the sub. " + - "It should be fine for the situation at hand, but you still need to find an oxygen tank and drag it into the same slot as the mask." + - "You should grab one or two from one of the cabinets."); - } - else if (HasItem("divingsuit")) - { - infoBox = CreateInfoFrame("", "In addition to letting you breathe underwater, the suit will protect you from the water pressure outside the sub " + - "(unlike the diving mask). However, you still need to drag an oxygen tank into the same slot as the suit to supply oxygen. " + - "You should grab one or two from one of the cabinets."); - } - - while (!HasItem("oxygentank")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - yield return new WaitForSeconds(5.0f); - - infoBox = CreateInfoFrame("", "Now you should stop the creature attacking the submarine before it does any more damage. Head to the railgun room at the upper right corner of the sub."); - - var railGun = Item.ItemList.Find(i => i.GetComponent() != null); - - while (Vector2.Distance(Controlled.Position, railGun.Position) > 500) - { - yield return new WaitForSeconds(1.0f); - } - - infoBox = CreateInfoFrame("", "The railgun requires a large power surge to fire. The reactor can't provide a surge large enough, so we need to use the " - + " supercapacitors in the railgun room. The capacitors need to be charged first; select them and crank up the recharge rate."); - - while (capacitor1.RechargeSpeed < 0.5f && capacitor2.RechargeSpeed < 0.5f) - { - yield return new WaitForSeconds(1.0f); - } - - infoBox = CreateInfoFrame("", "The capacitors take some time to recharge, so now is a good " + - "time to head to the room below and load some shells for the railgun."); - - - var loader = Item.ItemList.Find(i => i.Prefab.Identifier == "railgunloader").GetComponent(); - - while (Math.Abs(Controlled.Position.Y - loader.Item.Position.Y) > 80) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Grab one of the shells. You can load it by selecting the railgun loader and dragging the shell to. " - + "one of the free slots. You need two hands to carry a shell, so make sure you don't have anything else in either hand."); - - while (loader.Item.ContainedItems.FirstOrDefault(i => i != null && i.Prefab.Identifier == "railgunshell") == null) - { - //TODO: reimplement - //moloch.Health = 50.0f; - - capacitor1.Charge += 5.0f; - capacitor2.Charge += 5.0f; - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Now we're ready to shoot! Select the railgun controller."); - - while (Controlled.SelectedConstruction == null || Controlled.SelectedConstruction.Prefab.Identifier != "railguncontroller") - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - moloch.AnimController.SetPosition(ConvertUnits.ToSimUnits(Controlled.WorldPosition + Vector2.UnitY * 600.0f)); - - infoBox = CreateInfoFrame("", "Use the right mouse button to aim and wait for the creature to come closer. When you're ready to shoot, " - + "press the left mouse button."); - - while (!moloch.IsDead) - { - if (moloch.WorldPosition.Y > Controlled.WorldPosition.Y + 600.0f) - { - moloch.AIController.SteeringManager.SteeringManual(CoroutineManager.DeltaTime, Controlled.WorldPosition - moloch.WorldPosition); - } - - moloch.AIController.SelectTarget(Controlled.AiTarget); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - Submarine.MainSub.GodMode = false; - - infoBox = CreateInfoFrame("", "The creature has died. Now you should fix the damages in the control room: " + - "Grab a welding tool from the closet in the railgun room."); - - while (!HasItem("weldingtool")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The welding tool requires fuel to work. Grab a welding fuel tank and attach it to the tool " + - "by dragging it into the same slot."); - - do - { - var weldingTool = Controlled.Inventory.FindItemByIdentifier("weldingtool"); - if (weldingTool != null && - weldingTool.ContainedItems.FirstOrDefault(contained => contained != null && contained.Prefab.Identifier == "weldingfueltank") != null) break; - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } while (true); - - - infoBox = CreateInfoFrame("", "You can aim with the tool using the right mouse button and weld using the left button. " + - "Head to the command room to fix the leaks there."); - - do - { - broken = false; - foreach (Structure window in windows) - { - for (int i = 0; i < window.SectionCount; i++) - { - if (!window.SectionIsLeaking(i)) continue; - broken = true; - break; - } - if (broken) break; - } - - yield return new WaitForSeconds(1.0f); - } while (broken); - - infoBox = CreateInfoFrame("", "The hull is fixed now, but there's still quite a bit of water inside the sub. It should be pumped out " - + "using the bilge pump in the room at the bottom of the submarine."); - - Pump pump = Item.ItemList.Find(i => i.HasTag("tutorialpump")).GetComponent(); - - while (Vector2.Distance(Controlled.Position, pump.Item.Position) > 100.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The two pumps inside the ballast tanks " - + "are connected straight to the navigation terminal and can't be manually controlled unless you mess with their wiring, " + - "so you should only use the pump in the middle room to pump out the water. Select it, turn it on and adjust the pumping speed " + - "to start pumping water out.", hasButton: true); - - while (infoBox != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - - bool brokenMsgShown = false; - - Item brokenBox = null; - - while (pump.FlowPercentage > 0.0f || pump.CurrFlow <= 0.0f || !pump.IsActive) - { - if (!brokenMsgShown && pump.Voltage < pump.MinVoltage && Controlled.SelectedConstruction == pump.Item) - { - brokenMsgShown = true; - - infoBox = CreateInfoFrame("", "Looks like the pump isn't getting any power. The water must have short-circuited some of the junction " - + "boxes. You can check which boxes are broken by selecting them."); - - while (true) - { - if (Controlled.SelectedConstruction!=null && - Controlled.SelectedConstruction.GetComponent() != null && - Controlled.SelectedConstruction.Condition == 0.0f) - { - brokenBox = Controlled.SelectedConstruction; - - infoBox = CreateInfoFrame("", "Here's our problem: this junction box is broken. Luckily engineers are adept at fixing electrical devices - " - + "you just need to find a spare wire and click the \"Fix\"-button to repair the box."); - break; - } - - if (pump.Voltage > pump.MinVoltage) break; - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - } - - if (brokenBox != null && brokenBox.ConditionPercentage > 50.0f && pump.Voltage < pump.MinVoltage) - { - yield return new WaitForSeconds(1.0f); - - if (pump.Voltage < pump.MinVoltage) - { - infoBox = CreateInfoFrame("", "The pump is still not running. Check if there are more broken junction boxes between the pump and the reactor."); - } - brokenBox = null; - } - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The pump is up and running. Wait for the water to be drained out."); - - while (pump.Item.CurrentHull.WaterVolume > 1000.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "That was all there is to this tutorial! Now you should be able to handle " + - "most of the basic tasks on board the submarine."); - - Completed = true; - - yield return new WaitForSeconds(4.0f); - - Controlled = null; - GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; - GameMain.LightManager.LosEnabled = false; - - var cinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight, panDuration: 5.0f); - - while (cinematic.Running) - { - yield return Controlled != null && Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - Submarine.Unload(); - GameMain.MainMenuScreen.Select(); - - yield return CoroutineStatus.Success; - } - - private bool HasItem(string itemIdentifier) - { - if (Character.Controlled == null) return false; - - return Character.Controlled.Inventory.FindItemByIdentifier(itemIdentifier) != null; - } - - protected IEnumerable KeepReactorRunning(Reactor reactor) - { - do - { - //TODO: reimplement - /*reactor.AutoTemp = true; - reactor.ShutDownTemp = 5000.0f;*/ - - yield return CoroutineStatus.Running; - } while (Item.ItemList.Contains(reactor.Item)); - - yield return CoroutineStatus.Success; - } - - - /// - /// keeps the enemy away from the sub until the capacitors are loaded - /// - private IEnumerable KeepEnemyAway(Character enemy, PowerContainer[] capacitors) - { - do - { - if (enemy == null || Character.Controlled == null) break; - - //TODO: reimplement - //enemy.Health = 50.0f; - - if (enemy.AIController is EnemyAIController enemyAI) - { - enemyAI.State = AIState.Idle; - } - - Vector2 targetPos = Character.Controlled.WorldPosition + new Vector2(0.0f, 3000.0f); - - Vector2 steering = targetPos - enemy.WorldPosition; - if (steering != Vector2.Zero) steering = Vector2.Normalize(steering); - - enemy.AIController.Steering = steering; - - yield return CoroutineStatus.Running; - } while (capacitors.FirstOrDefault(c => c.Charge > 0.4f) == null); - - yield return CoroutineStatus.Success; - } - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 5ad157c47..d6435a6f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -41,30 +41,79 @@ namespace Barotrauma.Tutorials // Variables private Character captain; - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Sprite captain_steerIcon; private Color captain_steerIconColor; - public CaptainTutorial(XElement element) : base(element) + public CaptainTutorial() : base("tutorial.captaintraining".ToIdentifier(), + new Segment( + "Captain.CommandMedic".ToIdentifier(), + "Captain.CommandMedicObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.CommandMedicText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_command.webm", TextTag = "Captain.CommandMedicText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Captain.CommandMechanic".ToIdentifier(), + "Captain.CommandMechanicObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Captain.CommandMechanicText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Captain.CommandSecurity".ToIdentifier(), + "Captain.CommandSecurityObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Captain.CommandSecurityText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Captain.CommandEngineer".ToIdentifier(), + "Captain.CommandEngineerObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Captain.CommandEngineerText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Captain.Undock".ToIdentifier(), + "Captain.UndockObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.UndockText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_undock.webm", TextTag = "Captain.UndockText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Captain.Navigate".ToIdentifier(), + "Captain.NavigateObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.NavigateText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_navigation.webm", TextTag = "Captain.NavigateText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Captain.Dock".ToIdentifier(), + "Captain.DockObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.DockText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_docking.webm", TextTag = "Captain.DockText".ToIdentifier(), Width = 450, Height = 80 })) + { } + + protected override CharacterInfo GetCharacterInfo() { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["captain"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 20), + new Skill("weapons".ToIdentifier(), 20), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 70))); } - public override void Start() + protected override void Initialize() { - base.Start(); - captain = Character.Controlled; radioSpeakerName = TextManager.Get("Tutorial.Radio.Watchman"); GameMain.GameSession.CrewManager.AllowCharacterSwitch = false; - var revolver = FindOrGiveItem(captain, "revolver"); + var revolver = FindOrGiveItem(captain, "revolver".ToIdentifier()); revolver.Unequip(captain); captain.Inventory.RemoveItem(revolver); var captainscap = - captain.Inventory.FindItemByIdentifier("captainscap1") ?? - captain.Inventory.FindItemByIdentifier("captainscap2") ?? - captain.Inventory.FindItemByIdentifier("captainscap3"); + captain.Inventory.FindItemByIdentifier("captainscap1".ToIdentifier()) ?? + captain.Inventory.FindItemByIdentifier("captainscap2".ToIdentifier()) ?? + captain.Inventory.FindItemByIdentifier("captainscap3".ToIdentifier()); if (captainscap != null) { @@ -73,16 +122,16 @@ namespace Barotrauma.Tutorials } var captainsuniform = - captain.Inventory.FindItemByIdentifier("captainsuniform1") ?? - captain.Inventory.FindItemByIdentifier("captainsuniform2") ?? - captain.Inventory.FindItemByIdentifier("captainsuniform3"); + captain.Inventory.FindItemByIdentifier("captainsuniform1".ToIdentifier()) ?? + captain.Inventory.FindItemByIdentifier("captainsuniform2".ToIdentifier()) ?? + captain.Inventory.FindItemByIdentifier("captainsuniform3".ToIdentifier()); if (captainsuniform != null) { captainsuniform.Unequip(captain); captain.Inventory.RemoveItem(captainsuniform); } - var steerOrder = Order.GetPrefab("steer"); + var steerOrder = OrderPrefab.Prefabs["steer"]; captain_steerIcon = steerOrder.SymbolSprite; captain_steerIconColor = steerOrder.Color; @@ -99,7 +148,7 @@ namespace Barotrauma.Tutorials captain_medicSpawnPos = Item.ItemList.Find(i => i.HasTag("captain_medicspawnpos")).WorldPosition; tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("medicaldoctor")); + var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("medicaldoctor")); captain_medic = Character.Create(medicInfo, captain_medicSpawnPos, "medicaldoctor"); captain_medic.TeamID = CharacterTeamType.Team1; captain_medic.GiveJobItems(null); @@ -122,17 +171,17 @@ namespace Barotrauma.Tutorials SetDoorAccess(tutorial_lockedDoor_1, null, false); SetDoorAccess(tutorial_lockedDoor_2, null, false); - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("mechanic")); + var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("mechanic")); captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "mechanic"); captain_mechanic.TeamID = CharacterTeamType.Team1; captain_mechanic.GiveJobItems(); - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); + var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("securityofficer")); captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "securityofficer"); captain_security.TeamID = CharacterTeamType.Team1; captain_security.GiveJobItems(); - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); + var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")); captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "engineer"); captain_engineer.TeamID = CharacterTeamType.Team1; captain_engineer.GiveJobItems(); @@ -163,7 +212,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2f, false); GameMain.GameSession.CrewManager.AutoShowCrewList(); GameMain.GameSession.CrewManager.AddCharacter(captain_medic); - TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); do { yield return null; @@ -172,13 +221,13 @@ namespace Barotrauma.Tutorials } while (!HasOrder(captain_medic, "follow")); SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); // Submarine do { yield return null; } while (!captain_enteredSubmarineSensor.MotionDetected); yield return new WaitForSeconds(3f, false); captain_mechanic.AIController.Enabled = captain_security.AIController.Enabled = captain_engineer.AIController.Enabled = true; - TriggerTutorialSegment(1, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_mechanic); do { @@ -187,9 +236,9 @@ namespace Barotrauma.Tutorials // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_mechanic, "repairsystems", highlightColor, new Vector2(5, 5)); //HighlightOrderOption("jobspecific"); } while (!HasOrder(captain_mechanic, "repairsystems") && !HasOrder(captain_mechanic, "repairmechanical") && !HasOrder(captain_mechanic, "repairelectrical")); - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(2, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_security); do { @@ -199,9 +248,9 @@ namespace Barotrauma.Tutorials HighlightOrderOption("fireatwill"); } while (!HasOrder(captain_security, "operateweapons")); - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); yield return new WaitForSeconds(4f, false); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_engineer); do { @@ -211,7 +260,7 @@ namespace Barotrauma.Tutorials HighlightOrderOption("powerup"); } while (!HasOrder(captain_engineer, "operatereactor", "powerup")); - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); tutorial_submarineReactor.CanBeSelected = true; do { yield return null; } while (!tutorial_submarineReactor.IsActive); // Wait until reactor on TriggerTutorialSegment(4); @@ -226,7 +275,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(1.0f, false); } while (Submarine.MainSub.DockedTo.Any()); captain_navConsole.UseAutoDocking = false; - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(5); // Navigate to destination do @@ -241,7 +290,7 @@ namespace Barotrauma.Tutorials yield return null; } while (captain_sonar.CurrentMode != Sonar.Mode.Active); do { yield return null; } while (Vector2.Distance(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) > 4000f); - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); captain_navConsole.UseAutoDocking = true; yield return new WaitForSeconds(4f, false); TriggerTutorialSegment(6); // Docking @@ -250,7 +299,7 @@ namespace Barotrauma.Tutorials //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); } while (!Submarine.MainSub.AtEndExit || !Submarine.MainSub.DockedTo.Any()); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); yield return new WaitForSeconds(3f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.GetWithVariable("Captain.Radio.Complete", "[OUTPOSTNAME]", GameMain.GameSession.EndLocation.Name), ChatMessageType.Radio, null); SetHighlight(captain_navConsole.Item, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ContextualTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ContextualTutorial.cs deleted file mode 100644 index a05c6a835..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ContextualTutorial.cs +++ /dev/null @@ -1,520 +0,0 @@ -/*using System.Collections.Generic; -using System.Xml.Linq; -using System; -using Microsoft.Xna.Framework; -using Barotrauma.Items.Components; -using System.Linq; - -namespace Barotrauma.Tutorials -{ - class ContextualTutorial : Tutorial - { - public ContextualTutorial(XElement element) : base(element) - { - //Name = "ContextualTutorial"; - } - - public static bool Selected = false; - - private Steering navConsole; - private Reactor reactor; - private Sonar sonar; - private Vector2 subStartingPosition; - private List crew; - private Character mechanic; - private Character engineer; - private Character injuredMember = null; - - private List> characterTimeOnSonar; - private float requiredTimeOnSonar = 5f; - - private float tutorialTimer; - - private bool disableTutorialOnDeficiencyFound = true; - - private float floodTutorialTimer = 0.0f; - private const float floodTutorialDelay = 2.0f; - private float medicalTutorialTimer = 0.0f; - private const float medicalTutorialDelay = 2.0f; - - public override void Initialize() - { - base.Initialize(); - - for (int i = 0; i < segments.Count; i++) - { - segments[i].IsTriggered = false; - } - - characterTimeOnSonar = new List>(); - } - - public void LoadPartiallyComplete(XElement element) - { - int[] completedSegments = element.GetAttributeIntArray("completedsegments", null); - - if (completedSegments == null || completedSegments.Length == 0) - { - return; - } - - if (completedSegments.Length == segments.Count) // Completed all segments - { - Stop(); - return; - } - - for (int i = 0; i < completedSegments.Length; i++) - { - segments[completedSegments[i]].IsTriggered = true; - } - } - - public void SavePartiallyComplete(XElement element) - { - XElement tutorialElement = new XElement("contextualtutorial"); - tutorialElement.Add(new XAttribute("completedsegments", GetCompletedSegments())); - element.Add(tutorialElement); - } - - private string GetCompletedSegments() - { - string completedSegments = string.Empty; - - for (int i = 0; i < segments.Count; i++) - { - if (segments[i].IsTriggered) - { - completedSegments += i + ","; - } - } - - if (completedSegments.Length > 0) - { - completedSegments = completedSegments.TrimEnd(','); - } - - return completedSegments; - } - - public override void Start() - { - if (!Initialized) return; - - base.Start(); - injuredMember = null; - activeContentSegment = null; - tutorialTimer = floodTutorialTimer = medicalTutorialTimer = 0.0f; - subStartingPosition = Vector2.Zero; - characterTimeOnSonar.Clear(); - - subStartingPosition = Submarine.MainSub.WorldPosition; - navConsole = Item.ItemList.Find(i => i.HasTag("command"))?.GetComponent(); - sonar = navConsole?.Item.GetComponent(); - reactor = Item.ItemList.Find(i => i.HasTag("reactor"))?.GetComponent(); - -#if DEBUG - if (reactor == null || navConsole == null || sonar == null) - { - infoBox = CreateInfoFrame("Error", "Submarine not compatible with the tutorial:" - + "\nReactor - " + (reactor != null ? "OK" : "Tag 'reactor' not found") - + "\nNavigation Console - " + (navConsole != null ? "OK" : "Tag 'command' not found") - + "\nSonar - " + (sonar != null ? "OK" : "Not found under Navigation Console"), hasButton: true); - CoroutineManager.StartCoroutine(WaitForErrorClosed()); - return; - } -#endif - if (disableTutorialOnDeficiencyFound) - { - if (reactor == null || navConsole == null || sonar == null) - { - Stop(); - return; - } - } - else - { - if (navConsole == null) segments[2].IsTriggered = true; // Disable navigation console usage tutorial - if (reactor == null) segments[5].IsTriggered = true; // Disable reactor usage tutorial - if (sonar == null) segments[6].IsTriggered = true; // Disable enemy on sonar tutorial - } - - crew = GameMain.GameSession.CrewManager.GetCharacters().ToList(); - mechanic = CrewMemberWithJob("mechanic"); - engineer = CrewMemberWithJob("engineer"); - - Completed = true; // Trigger completed at start to prevent the contextual tutorial from automatically activating on starting new campaigns after this one - started = true; - } - -#if DEBUG - private IEnumerable WaitForErrorClosed() - { - while (infoBox != null) yield return null; - Stop(); - } -#endif - - public override void Stop() - { - base.Stop(); - characterTimeOnSonar = null; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - - if (!started || ContentRunning) return; - - deltaTime *= 0.5f; - - for (int i = 0; i < segments.Count; i++) - { - if (segments[i].IsTriggered || HasObjective(segments[i])) continue; - if (CheckContextualTutorials(i, deltaTime)) // Found a relevant tutorial, halt finding new ones - { - break; - } - } - } - - private bool CheckContextualTutorials(int index, float deltaTime) - { - switch (index) - { - case 0: // Welcome: Game Start [Text] - if (tutorialTimer < 1.0f) - { - tutorialTimer += deltaTime; - return false; - } - break; - case 1: // Command Reactor: 2 seconds after 'Welcome' dismissed and only if no command given to start reactor [Video] - if (!segments[0].IsTriggered) return false; - if (tutorialTimer < 3.0f) - { - tutorialTimer += deltaTime; - - if (HasOrder("operatereactor")) - { - segments[index].IsTriggered = true; - tutorialTimer = 2.5f; - } - return false; - } - break; - case 2: // Nav Console: 2 seconds after 'Command Reactor' dismissed or if nav console is activated [Video] - if (!IsReactorPoweredUp()) return false; // Do not advance tutorial based on this segment if reactor has not been powered up - if (Character.Controlled?.SelectedConstruction != navConsole.Item) - { - if (tutorialTimer < 4.5f) - { - tutorialTimer += deltaTime; - return false; - } - } - else - { - tutorialTimer = 4.5f; - } - - TriggerTutorialSegment(index, GameMain.GameSession.EndLocation.Name); - return true; - case 3: // Objective: Travel ~150 meters and while sub is not flooding [Text] - if (Vector2.Distance(subStartingPosition, Submarine.MainSub.WorldPosition) < 8000f || IsFlooding()) - { - return false; - } - else // Called earlier than others due to requiring specific args - { - TriggerTutorialSegment(index, GameMain.GameSession.EndLocation.Name); - return true; - } - case 4: // Flood: Hull is breached and sub is taking on water [Video] - if (!IsFlooding()) - { - return false; - } - else if (floodTutorialTimer < floodTutorialDelay) - { - floodTutorialTimer += deltaTime; - return false; - } - break; - case 5: // Reactor: Player uses reactor for the first time [Video] - if (Character.Controlled?.SelectedConstruction != reactor.Item) - { - return false; - } - break; - case 6: // Enemy on Sonar: Player witnesses creature signal on sonar for 5 seconds [Video] - if (!HasEnemyOnSonarForDuration(deltaTime)) - { - return false; - } - break; - case 7: // Degrading1: Any equipment degrades to 50% health or less and player has not assigned any crew to perform maintenance [Text] - if ((mechanic == null || mechanic.IsDead) && (engineer == null || engineer.IsDead)) // Both engineer and mechanic are dead or do not exist -> do not display - { - return false; - } - - bool degradedEquipmentFound = false; - - foreach (Item item in Item.ItemList) - { - if (!item.Repairables.Any() || item.Condition > 50.0f) continue; - degradedEquipmentFound = true; - break; - } - - if (degradedEquipmentFound) - { - if (HasOrder("repairsystems", "jobspecific")) - { - segments[index].IsTriggered = true; - return false; - } - } - else - { - return false; - } - break; - case 8: // Medical: Crewmember is injured but not killed [Video] - - if (injuredMember == null) - { - for (int i = 0; i < crew.Count; i++) - { - Character member = crew[i]; - if (member.Vitality < member.MaxVitality && !member.IsDead) - { - injuredMember = member; - break; - } - } - - return false; - } - else if (medicalTutorialTimer < medicalTutorialDelay) - { - medicalTutorialTimer += deltaTime; - return false; - } - else - { - TriggerTutorialSegment(index, new string[] { injuredMember.Info.DisplayName, - (injuredMember.Info.Gender == Gender.Male) ? TextManager.Get("PronounPossessiveMale").ToLower() : TextManager.Get("PronounPossessiveFemale").ToLower() }); - return true; - } - case 9: // Approach1: Destination is within ~100m [Video] - if (Vector2.Distance(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) > 8000f) - { - return false; - } - else - { - TriggerTutorialSegment(index, GameMain.GameSession.EndLocation.Name); - return true; - } - case 10: // Approach2: Sub is docked [Text] - if (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Count == 0) - { - return false; - } - break; - } - - TriggerTutorialSegment(index); - return true; - } - - protected override void CheckActiveObjectives(TutorialSegment objective, float deltaTime) - { - switch(objective.Id) - { - case "ReactorCommand": // Reactor commanded - if (!IsReactorPoweredUp()) - { - if (!HasOrder("operatereactor")) return; - } - break; - case "NavConsole": // traveled 50 meters - if (Vector2.Distance(subStartingPosition, Submarine.MainSub.WorldPosition) < 4000f) - { - return; - } - break; - case "Flood": // Hull breaches repaired - if (IsFlooding()) return; - break; - case "Medical": - if (injuredMember != null && !injuredMember.IsDead) - { - if (injuredMember.CharacterHealth.DroppedItem == null) return; - } - break; - case "EnemyOnSonar": // Enemy dispatched - if (HasEnemyOnSonarForDuration(deltaTime)) - { - return; - } - break; - case "Degrading": // Fixed - if (mechanic != null && !mechanic.IsDead) - { - HumanAIController humanAI = mechanic.AIController as HumanAIController; - if (mechanic.CurrentOrder?.AITag != "repairsystems" || humanAI.CurrentOrderOption != "jobspecific") - { - return; - } - } - - if (engineer != null && !engineer.IsDead) - { - HumanAIController humanAI = engineer.AIController as HumanAIController; - if (engineer.CurrentOrder?.AITag != "repairsystems" || humanAI.CurrentOrderOption != "jobspecific") - { - return; - } - } - - break; - case "Approach1": // Wait until docked - if (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Count == 0) - { - return; - } - break; - } - - RemoveCompletedObjective(objective); - } - - private bool IsReactorPoweredUp() - { - float load = 0.0f; - List connections = reactor.Item.Connections; - if (connections != null && connections.Count > 0) - { - foreach (Connection connection in connections) - { - if (!connection.IsPower) continue; - foreach (Connection recipient in connection.Recipients) - { - if (!(recipient.Item is Item it)) continue; - - PowerTransfer pt = it.GetComponent(); - if (pt == null) continue; - - load = Math.Max(load, pt.PowerLoad); - } - } - } - - return Math.Abs(load + reactor.CurrPowerConsumption) < 10; - } - - private Character CrewMemberWithJob(string job) - { - job = job.ToLowerInvariant(); - for (int i = 0; i < crew.Count; i++) - { - if (crew[i].Info.Job.Prefab.Identifier.ToLowerInvariant() == job) return crew[i]; - } - - return null; - } - - private bool HasOrder(string aiTag, string option = null) - { - for (int i = 0; i < crew.Count; i++) - { - if (crew[i].CurrentOrder?.AITag == aiTag) - { - if (option == null) - { - return true; - } - else - { - HumanAIController humanAI = crew[i].AIController as HumanAIController; - return humanAI.CurrentOrderOption == option; - } - } - } - - return false; - } - - private bool IsFlooding() - { - foreach (Gap gap in Gap.GapList) - { - if (gap.ConnectedWall == null || gap.IsRoomToRoom) continue; - if (gap.ConnectedDoor != null || gap.Open <= 0.0f) continue; - if (gap.Submarine == null) continue; - if (gap.Submarine.IsOutpost) continue; - if (gap.Submarine != Submarine.MainSub) continue; - if (gap.FlowTargetHull == null || gap.FlowTargetHull.WaterPercentage <= 0.0f) continue; - return true; - } - - return false; - } - - private bool HasEnemyOnSonarForDuration(float deltaTime) - { - foreach (Character c in Character.CharacterList) - { - if (c.AnimController.CurrentHull != null || !c.Enabled || !(c.AIController is EnemyAIController)) continue; - if (sonar.DetectSubmarineWalls && c.AnimController.CurrentHull == null && sonar.Item.CurrentHull != null) continue; - if (Vector2.DistanceSquared(c.WorldPosition, sonar.Item.WorldPosition) > sonar.Range * sonar.Range) - { - for (int i = 0; i < characterTimeOnSonar.Count; i++) - { - if (characterTimeOnSonar[i].First == c) - { - characterTimeOnSonar.RemoveAt(i); - break; - } - } - - continue; - } - - Pair pair = characterTimeOnSonar.Find(ct => ct.First == c); - if (pair != null) - { - pair.Second += deltaTime; - } - else - { - characterTimeOnSonar.Add(new Pair(c, deltaTime)); - } - } - - return characterTimeOnSonar.Find(ct => ct.Second >= requiredTimeOnSonar && !ct.First.IsDead) != null; - } - - protected override void TriggerTutorialSegment(int index, params object[] args) - { - base.TriggerTutorialSegment(index, args); - - for (int i = 0; i < segments.Count; i++) - { - if (!segments[i].IsTriggered) return; - } - - CoroutineManager.StartCoroutine(WaitToStop()); // Completed - } - - private IEnumerable WaitToStop() - { - while (ContentRunning) yield return null; - Stop(); - } - } -}*/ diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 641b28ad8..8b46096c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Tutorials private float shakeTimer = 1f; private float shakeAmount = 20f; - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character doctor; private ItemContainer doctor_suppliesCabinet; @@ -40,14 +40,66 @@ namespace Barotrauma.Tutorials private Sprite doctor_firstAidIcon; private Color doctor_firstAidIconColor; - public DoctorTutorial(XElement element) : base(element) - { - } - public override void Start() - { - base.Start(); - var firstAidOrder = Order.GetPrefab("requestfirstaid"); + public DoctorTutorial() : base("tutorial.medicaldoctortraining".ToIdentifier(), + new Segment( + "Doctor.Supplies".ToIdentifier(), + "Doctor.SuppliesObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Doctor.SuppliesText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Doctor.OpenMedicalInterface".ToIdentifier(), + "Doctor.OpenMedicalInterfaceObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.OpenMedicalInterfaceText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_medinterface1.webm", TextTag = "Doctor.OpenMedicalInterfaceText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.FirstAidSelf".ToIdentifier(), + "Doctor.FirstAidSelfObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.FirstAidSelfText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_medinterface1.webm", TextTag = "Doctor.FirstAidSelfText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.Medbay".ToIdentifier(), + "Doctor.MedbayObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.MedbayText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_command.webm", TextTag = "Doctor.MedbayText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.TreatBurns".ToIdentifier(), + "Doctor.TreatBurnsObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.TreatBurnsText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_medinterface2.webm", TextTag = "Doctor.TreatBurnsText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.CPR".ToIdentifier(), + "Doctor.CPRObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.CPRText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_cpr.webm", TextTag = "Doctor.CPRText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.Submarine".ToIdentifier(), + "Doctor.SubmarineObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Doctor.SubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) + { } + + protected override CharacterInfo GetCharacterInfo() + { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 70), + new Skill("weapons".ToIdentifier(), 20), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 20))); + } + + protected override void Initialize() + { + var firstAidOrder = OrderPrefab.Prefabs["requestfirstaid"]; doctor_firstAidIcon = firstAidOrder.SymbolSprite; doctor_firstAidIconColor = firstAidOrder.Color; @@ -55,19 +107,19 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); doctor = Character.Controlled; - var bandages = FindOrGiveItem(doctor, "antibleeding1"); + var bandages = FindOrGiveItem(doctor, "antibleeding1".ToIdentifier()); bandages.Unequip(doctor); doctor.Inventory.RemoveItem(bandages); - var syringegun = FindOrGiveItem(doctor, "syringegun"); + var syringegun = FindOrGiveItem(doctor, "syringegun".ToIdentifier()); syringegun.Unequip(doctor); doctor.Inventory.RemoveItem(syringegun); - var antibiotics = FindOrGiveItem(doctor, "antibiotics"); + var antibiotics = FindOrGiveItem(doctor, "antibiotics".ToIdentifier()); antibiotics.Unequip(doctor); doctor.Inventory.RemoveItem(antibiotics); - var morphine = FindOrGiveItem(doctor, "antidama1"); + var morphine = FindOrGiveItem(doctor, "antidama1".ToIdentifier()); morphine.Unequip(doctor); doctor.Inventory.RemoveItem(morphine); @@ -78,7 +130,7 @@ namespace Barotrauma.Tutorials var patientHull2 = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "airlock").CurrentHull; medBay = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "medbay").CurrentHull; - var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("assistant")); + var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("assistant")); patient1 = Character.Create(assistantInfo, patientHull1.WorldPosition, "1"); patient1.TeamID = CharacterTeamType.Team1; patient1.GiveJobItems(null); @@ -86,26 +138,26 @@ namespace Barotrauma.Tutorials patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 15.0f) }, stun: 0, playSound: false); patient1.AIController.Enabled = false; - assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("assistant")); + assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("assistant")); patient2 = Character.Create(assistantInfo, patientHull2.WorldPosition, "2"); patient2.TeamID = CharacterTeamType.Team1; patient2.GiveJobItems(null); patient2.CanSpeak = false; patient2.AIController.Enabled = false; - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); + var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")); var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient1.TeamID = CharacterTeamType.Team1; subPatient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient1); - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); + var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("securityofficer")); var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient2.TeamID = CharacterTeamType.Team1; subPatient2.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.InternalDamage, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient2); - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); + var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")); var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient3.TeamID = CharacterTeamType.Team1; subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); @@ -196,7 +248,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2.0f); }*/ - TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Deselect), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Medical supplies objective + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Medical supplies objective do { @@ -215,24 +267,24 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (doctor.Inventory.FindItemByIdentifier("antidama1") == null); // Wait until looted + } while (doctor.Inventory.FindItemByIdentifier("antidama1".ToIdentifier()) == null); // Wait until looted yield return new WaitForSeconds(1.0f, false); SetHighlight(doctor_suppliesCabinet.Item, false); - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); yield return new WaitForSeconds(1.0f, false); // 2nd tutorial segment, treat self ------------------------------------------------------------------------- - TriggerTutorialSegment(1, GameMain.Config.KeyBindText(InputType.Health)); // Open health interface + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // Open health interface while (CharacterHealth.OpenHealthWindow == null) { doctor.CharacterHealth.HealthBarPulsateTimer = 1.0f; yield return null; } yield return null; - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); yield return new WaitForSeconds(1.0f, false); TriggerTutorialSegment(2); //Treat self while (doctor.CharacterHealth.GetAfflictionStrength("damage") > 0.01f) @@ -243,13 +295,13 @@ namespace Barotrauma.Tutorials } else { - HighlightInventorySlot(doctor.Inventory, "antidama1", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(doctor.Inventory, "antidama1".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; } - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); SetDoorAccess(doctor_firstDoor, doctor_firstDoorLight, true); while (CharacterHealth.OpenHealthWindow != null) @@ -260,10 +312,10 @@ namespace Barotrauma.Tutorials // treat patient -------------------------------------------------------------------------------------------- //patient 1 requests first aid - var newOrder = new Order(Order.GetPrefab("requestfirstaid"), patient1.CurrentHull, null, orderGiver: patient1); + var newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], patient1.CurrentHull, null, orderGiver: patient1); doctor.AddActiveObjectiveEntity(patient1, doctor_firstAidIcon, doctor_firstAidIconColor); //GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient1.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order, null); + GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient1.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName?.Value, givingOrderToSelf: false), ChatMessageType.Order, null); while (doctor.CurrentHull != patient1.CurrentHull) { @@ -281,9 +333,9 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(3.0f, false); patient1.AIController.Enabled = true; doctor.RemoveActiveObjectiveEntity(patient1); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); // Get the patient to medbay + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); // Get the patient to medbay - while (patient1.GetCurrentOrderWithTopPriority()?.Order?.Identifier != "follow") + while (patient1.GetCurrentOrderWithTopPriority()?.Identifier != "follow") { // TODO: Rework order highlighting for new command UI // GameMain.GameSession.CrewManager.HighlightOrderButton(patient1, "follow", highlightColor, new Vector2(5, 5)); @@ -296,14 +348,14 @@ namespace Barotrauma.Tutorials { yield return new WaitForSeconds(1.0f, false); } - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); SetHighlight(doctor_medBayCabinet.Item, true); SetDoorAccess(doctor_thirdDoor, doctor_thirdDoorLight, true); patient1.CharacterHealth.UseHealthWindow = true; yield return new WaitForSeconds(2.0f, false); - TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Health)); // treat burns + TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // treat burns do { @@ -322,7 +374,7 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (doctor.Inventory.FindItemByIdentifier("antibleeding1") == null); // Wait until looted + } while (doctor.Inventory.FindItemByIdentifier("antibleeding1".ToIdentifier()) == null); // Wait until looted SetHighlight(doctor_medBayCabinet.Item, false); SetHighlight(patient1, true); @@ -334,12 +386,12 @@ namespace Barotrauma.Tutorials } else { - HighlightInventorySlot(doctor.Inventory, "antibleeding1", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(doctor.Inventory, "antibleeding1".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; } - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); SetHighlight(patient1, false); yield return new WaitForSeconds(1.0f, false); @@ -350,10 +402,10 @@ namespace Barotrauma.Tutorials //patient calls for help //patient2.CanSpeak = true; yield return new WaitForSeconds(2.0f, false); - newOrder = new Order(Order.GetPrefab("requestfirstaid"), patient2.CurrentHull, null, orderGiver: patient2); + newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], patient2.CurrentHull, null, orderGiver: patient2); doctor.AddActiveObjectiveEntity(patient2, doctor_firstAidIcon, doctor_firstAidIconColor); //GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient2.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order, null); + GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient2.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName?.Value, givingOrderToSelf: false), ChatMessageType.Order, null); patient2.AIController.Enabled = true; patient2.Oxygen = -50; CoroutineManager.StartCoroutine(KeepPatientAlive(patient2), "KeepPatient2Alive"); @@ -365,7 +417,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!tutorial_upperFinalDoor.IsOpen); yield return new WaitForSeconds(2.0f, false); - TriggerTutorialSegment(5, GameMain.Config.KeyBindText(InputType.Health)); // perform CPR + TriggerTutorialSegment(5, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // perform CPR SetHighlight(patient2, true); while (patient2.IsUnconscious) { @@ -380,7 +432,7 @@ namespace Barotrauma.Tutorials } yield return null; } - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); SetHighlight(patient2, false); doctor.RemoveActiveObjectiveEntity(patient2); CoroutineManager.StopCoroutines("KeepPatient2Alive"); @@ -399,7 +451,7 @@ namespace Barotrauma.Tutorials GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Doctor.Radio.EnteredSub"), ChatMessageType.Radio, null); yield return new WaitForSeconds(3.0f, false); - TriggerTutorialSegment(6, GameMain.Config.KeyBindText(InputType.Health)); // give treatment to anyone in need + TriggerTutorialSegment(6, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // give treatment to anyone in need foreach (var patient in subPatients) { @@ -421,8 +473,8 @@ namespace Barotrauma.Tutorials if (!patientCalledHelp[i] && Timing.TotalTime > subEnterTime + 60 * (i + 1)) { doctor.AddActiveObjectiveEntity(subPatients[i], doctor_firstAidIcon, doctor_firstAidIconColor); - newOrder = new Order(Order.GetPrefab("requestfirstaid"), subPatients[i].CurrentHull, null, orderGiver: subPatients[i]); - string message = newOrder.GetChatMessage("", subPatients[i].CurrentHull?.DisplayName, givingOrderToSelf: false); + newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], subPatients[i].CurrentHull, null, orderGiver: subPatients[i]); + string message = newOrder.GetChatMessage("", subPatients[i].CurrentHull?.DisplayName?.Value, givingOrderToSelf: false); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(subPatients[i].Name, message, ChatMessageType.Order, null); patientCalledHelp[i] = true; } @@ -435,7 +487,7 @@ namespace Barotrauma.Tutorials } yield return new WaitForSeconds(1.0f, false); } - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); foreach (var patient in subPatients) { SetHighlight(patient, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs deleted file mode 100644 index d2591a2b1..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Tutorials -{ - class EditorTutorial : Tutorial - { - public EditorTutorial(XElement element) - : base (element) - { - } - - public override IEnumerable UpdateState() - { - /*infoBox = CreateInfoFrame("Use the mouse wheel to zoom in and out, and WASD to move the camera around.", true); - - while (infoBox != null) - { - yield return CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("Press \"Structure\" at the left side of the screen to start placing some walls."); - - while (GameMain.SubEditorScreen.SelectedTab != (int)MapEntityCategory.Structure) - { - yield return CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("Select \"topwall\" from the list.", true); - - while (MapEntityPrefab.Selected == null || MapEntityPrefab.Selected.Name != "topwall") - { - yield return CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("You can now create a horizontal wall by clicking and dragging. When you're done, right click to stop creating walls.");*/ - - - - - yield return CoroutineStatus.Success; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index a380ef67e..da8a40fad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Tutorials private Reactor engineer_submarineReactor; // Variables - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character engineer; private int[] reactorLoads = new int[5] { 1500, 3000, 2000, 5000, 3500 }; private float reactorLoadChangeTime = 2f; @@ -75,27 +75,74 @@ namespace Barotrauma.Tutorials private Color engineer_reactorIconColor; private bool wiringActive = false; - public EngineerTutorial(XElement element) : base(element) - { + public EngineerTutorial() : base("tutorial.engineertraining".ToIdentifier(), + new Segment( + "Mechanic.Equipment".ToIdentifier(), + "Mechanic.EquipmentObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Engineer.Reactor".ToIdentifier(), + "Engineer.ReactorObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_reactor.webm", TextTag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80 }), + new Segment( + "Engineer.OperateReactor".ToIdentifier(), + "Engineer.OperateReactorObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Engineer.OperateReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_reactor.webm", TextTag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80 }), + new Segment( + "Engineer.RepairJunctionBox".ToIdentifier(), + "Engineer.RepairJunctionBoxObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Engineer.RepairJunctionBoxText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Engineer.WireJunctionBoxes".ToIdentifier(), + "Engineer.WireJunctionBoxesObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Engineer.WireJunctionBoxesText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_wiring.webm", TextTag = "Engineer.WireJunctionBoxesText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Engineer.RepairElectricalRoom".ToIdentifier(), + "Engineer.RepairElectricalRoomObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Engineer.RepairElectricalRoomText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Engineer.PowerUpReactor".ToIdentifier(), + "Engineer.PowerUpReactorObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Engineer.PowerUpReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center })) + { } + protected override CharacterInfo GetCharacterInfo() + { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 0), + new Skill("weapons".ToIdentifier(), 0), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 60), + new Skill("helm".ToIdentifier(), 0))); } - public override void Start() + protected override void Initialize() { - base.Start(); - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); engineer = Character.Controlled; - var toolbelt = FindOrGiveItem(engineer, "toolbelt"); + var toolbelt = FindOrGiveItem(engineer, "toolbelt".ToIdentifier()); toolbelt.Unequip(engineer); engineer.Inventory.RemoveItem(toolbelt); - var repairOrder = Order.GetPrefab("repairsystems"); + var repairOrder = OrderPrefab.Prefabs["repairsystems"]; engineer_repairIcon = repairOrder.SymbolSprite; engineer_repairIconColor = repairOrder.Color; - var reactorOrder = Order.GetPrefab("operatereactor"); + var reactorOrder = OrderPrefab.Prefabs["operatereactor"]; engineer_reactorIcon = reactorOrder.SymbolSprite; engineer_reactorIconColor = reactorOrder.Color; @@ -235,7 +282,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_equipmentObjectiveSensor.MotionDetected); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Equipment"), ChatMessageType.Radio, null); yield return new WaitForSeconds(0.5f, false); - TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Deselect), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Retrieve equipment + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Retrieve equipment bool firstSlotRemoved = false; bool secondSlotRemoved = false; bool thirdSlotRemoved = false; @@ -276,7 +323,7 @@ namespace Barotrauma.Tutorials yield return null; } while (!engineer_equipmentCabinet.Inventory.IsEmpty()); // Wait until looted - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); SetHighlight(engineer_equipmentCabinet.Item, false); SetHighlight(engineer_reactor.Item, true); SetDoorAccess(engineer_firstDoor, engineer_firstDoorLight, true); @@ -302,7 +349,7 @@ namespace Barotrauma.Tutorials if (IsSelectedItem(engineer_reactor.Item) && engineer_reactor.Item.OwnInventory.visualSlots != null) { engineer_reactor.AutoTemp = false; - HighlightInventorySlot(engineer.Inventory, "fuelrod", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(engineer.Inventory, "fuelrod".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); for (int i = 0; i < engineer_reactor.Item.OwnInventory.visualSlots.Length; i++) { @@ -311,7 +358,7 @@ namespace Barotrauma.Tutorials } yield return null; } while (engineer_reactor.AvailableFuel == 0); - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); TriggerTutorialSegment(2); CoroutineManager.StartCoroutine(ReactorOperatedProperly()); do @@ -354,7 +401,7 @@ namespace Barotrauma.Tutorials } while (wait > 0.0f); engineer.SelectedConstruction = null; engineer_reactor.CanBeSelected = false; - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); SetHighlight(engineer_reactor.Item, false); SetHighlight(engineer_brokenJunctionBox, true); SetDoorAccess(engineer_secondDoor, engineer_secondDoorLight, true); @@ -363,12 +410,12 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_secondDoor.IsOpen); yield return new WaitForSeconds(1f, false); Repairable repairableJunctionBoxComponent = engineer_brokenJunctionBox.GetComponent(); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Select)); // Repair the junction box + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Repair the junction box do { - if (!engineer.HasEquippedItem("screwdriver")) + if (!engineer.HasEquippedItem("screwdriver".ToIdentifier())) { - HighlightInventorySlot(engineer.Inventory, "screwdriver", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(engineer.Inventory, "screwdriver".ToIdentifier(), highlightColor, .5f, .5f, 0f); } else if (IsSelectedItem(engineer_brokenJunctionBox) && repairableJunctionBoxComponent.CurrentFixer == null) { @@ -380,7 +427,7 @@ namespace Barotrauma.Tutorials yield return null; } while (repairableJunctionBoxComponent.IsBelowRepairThreshold); // Wait until repaired SetHighlight(engineer_brokenJunctionBox, false); - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true); for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) { @@ -391,14 +438,14 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_thirdDoor.IsOpen); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.FaultyWiring"), ChatMessageType.Radio, null); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Use), GameMain.Config.KeyBindText(InputType.Deselect)); // Connect the junction boxes + TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Connect the junction boxes do { CheckGhostWires(); HandleJunctionBoxWiringHighlights(); yield return null; } while (engineer_workingPump.Voltage < engineer_workingPump.MinVoltage); // Wait until connected all the way to the pump CheckGhostWires(); for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) { SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, false); } - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); do { yield return null; } while (engineer_workingPump.Item.CurrentHull.WaterPercentage > waterVolumeBeforeOpening); // Wait until drained wiringActive = false; SetDoorAccess(engineer_fourthDoor, engineer_fourthDoorLight, true); @@ -424,7 +471,7 @@ namespace Barotrauma.Tutorials // Remove highlights when each individual machine is repaired do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (repairableJunctionBoxComponent1.IsBelowRepairThreshold || repairableJunctionBoxComponent2.IsBelowRepairThreshold || repairableJunctionBoxComponent3.IsBelowRepairThreshold); CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(6); // Powerup reactor @@ -433,7 +480,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!IsReactorPoweredUp(engineer_submarineReactor)); // Wait until ~matches load engineer.RemoveActiveObjectiveEntity(engineer_submarineReactor.Item); SetHighlight(engineer_submarineReactor.Item, false); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Complete"), ChatMessageType.Radio, null); yield return new WaitForSeconds(4f, false); @@ -516,9 +563,9 @@ namespace Barotrauma.Tutorials { Item selected = engineer.SelectedConstruction; - if (!engineer.HasEquippedItem("screwdriver")) + if (!engineer.HasEquippedItem("screwdriver".ToIdentifier())) { - HighlightInventorySlot(engineer.Inventory, "screwdriver", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(engineer.Inventory, "screwdriver".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } int selectedIndex = -1; @@ -537,9 +584,9 @@ namespace Barotrauma.Tutorials wiringActive = selectedIndex != -1; - if (!engineer.HasEquippedItem("wire")) + if (!engineer.HasEquippedItem("wire".ToIdentifier())) { - HighlightInventorySlotWithTag(engineer.Inventory, "wire", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlotWithTag(engineer.Inventory, "wire".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 11227ac0e..b26b2d32e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -69,33 +69,106 @@ namespace Barotrauma.Tutorials // Variables private const float waterVolumeBeforeOpening = 15f; - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character mechanic; private Sprite mechanic_repairIcon; private Color mechanic_repairIconColor; private Sprite mechanic_weldIcon; - public MechanicTutorial(XElement element) : base(element) - { + public MechanicTutorial() : base("tutorial.mechanictraining".ToIdentifier(), + new Segment( + "Mechanic.OpenDoor".ToIdentifier(), + "Mechanic.OpenDoorObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.OpenDoorText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.Equipment".ToIdentifier(), + "Mechanic.EquipmentObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_inventory.webm", TextTag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Welding".ToIdentifier(), + "Mechanic.WeldingObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.WeldingText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_equip.webm", TextTag = "Mechanic.WeldingText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Drain".ToIdentifier(), + "Mechanic.DrainObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.DrainText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.Deconstruct".ToIdentifier(), + "Mechanic.DeconstructObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.DeconstructText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_deconstruct.webm", TextTag = "Mechanic.DeconstructText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Fabricate".ToIdentifier(), + "Mechanic.FabricateObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.FabricateText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_fabricate.webm", TextTag = "Mechanic.FabricateText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Extinguisher".ToIdentifier(), + "Mechanic.ExtinguisherObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.ExtinguisherText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.DropExtinguisher".ToIdentifier(), + "Mechanic.DropExtinguisherObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.DropExtinguisherText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.Diving".ToIdentifier(), + "Mechanic.DivingObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.DivingText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.RepairPump".ToIdentifier(), + "Mechanic.RepairPumpObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.RepairPumpText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.RepairSubmarine".ToIdentifier(), + "Mechanic.RepairSubmarineObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.RepairSubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "tutorial.laddertitle".ToIdentifier(), + "tutorial.laddertitle".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "tutorial.ladderdescription".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) + { } + protected override CharacterInfo GetCharacterInfo() + { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 0), + new Skill("weapons".ToIdentifier(), 0), + new Skill("mechanical".ToIdentifier(), 50), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 0))); } - public override void Start() + protected override void Initialize() { - base.Start(); - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); mechanic = Character.Controlled; - var toolbelt = FindOrGiveItem(mechanic, "toolbelt"); + var toolbelt = FindOrGiveItem(mechanic, "toolbelt".ToIdentifier()); toolbelt.Unequip(mechanic); mechanic.Inventory.RemoveItem(toolbelt); - var crowbar = FindOrGiveItem(mechanic, "crowbar"); + var crowbar = FindOrGiveItem(mechanic, "crowbar".ToIdentifier()); crowbar.Unequip(mechanic); mechanic.Inventory.RemoveItem(crowbar); - var repairOrder = Order.GetPrefab("repairsystems"); + var repairOrder = OrderPrefab.Prefabs["repairsystems"]; mechanic_repairIcon = repairOrder.SymbolSprite; mechanic_repairIconColor = repairOrder.Color; mechanic_weldIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(1, 256, 127, 127), new Vector2(0.5f, 0.5f)); @@ -239,24 +312,25 @@ namespace Barotrauma.Tutorials } yield return new WaitForSeconds(2.5f, false); - mechanic_fabricator.RemoveFabricationRecipes(new List() { "extinguisher", "wrench", "weldingtool", "weldingfuel", "divingmask", "railgunshell", "nuclearshell", "uex", "harpoongun" }); + mechanic_fabricator.RemoveFabricationRecipes(allowedIdentifiers: + new[] { "extinguisher", "wrench", "weldingtool", "weldingfuel", "divingmask", "railgunshell", "nuclearshell", "uex", "harpoongun" }.ToIdentifiers()); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.WakeUp"), ChatMessageType.Radio, null); yield return new WaitForSeconds(2.5f, false); - TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Up), GameMain.Config.KeyBindText(InputType.Left), GameMain.Config.KeyBindText(InputType.Down), GameMain.Config.KeyBindText(InputType.Right), GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Select)); // Open door objective + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Up), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Left), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Down), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Right), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Open door objective yield return new WaitForSeconds(0.0f, false); SetDoorAccess(mechanic_firstDoor, mechanic_firstDoorLight, true); SetHighlight(mechanic_firstDoor.Item, true); do { yield return null; } while (!mechanic_firstDoor.IsOpen); SetHighlight(mechanic_firstDoor.Item, false); yield return new WaitForSeconds(1.5f, false); - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); // Room 2 yield return new WaitForSeconds(0.0f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Equipment"), ChatMessageType.Radio, null); do { yield return null; } while (!mechanic_equipmentObjectiveSensor.MotionDetected); - TriggerTutorialSegment(1, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Deselect), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Equipment & inventory objective + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Equipment & inventory objective SetHighlight(mechanic_equipmentCabinet.Item, true); bool firstSlotRemoved = false; bool secondSlotRemoved = false; @@ -290,35 +364,35 @@ namespace Barotrauma.Tutorials } yield return null; - } while (mechanic.Inventory.FindItemByIdentifier("divingmask") == null || mechanic.Inventory.FindItemByIdentifier("weldingtool") == null || mechanic.Inventory.FindItemByIdentifier("wrench") == null); // Wait until looted + } while (mechanic.Inventory.FindItemByIdentifier("divingmask".ToIdentifier()) == null || mechanic.Inventory.FindItemByIdentifier("weldingtool".ToIdentifier()) == null || mechanic.Inventory.FindItemByIdentifier("wrench".ToIdentifier()) == null); // Wait until looted SetHighlight(mechanic_equipmentCabinet.Item, false); yield return new WaitForSeconds(1.5f, false); - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Breach"), ChatMessageType.Radio, null); // Room 3 do { yield return null; } while (!mechanic_weldingObjectiveSensor.MotionDetected); - TriggerTutorialSegment(2, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Welding objective + TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Welding objective do { - if (!mechanic.HasEquippedItem("divingmask")) + if (!mechanic.HasEquippedItem("divingmask".ToIdentifier())) { - HighlightInventorySlot(mechanic.Inventory, "divingmask", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "divingmask".ToIdentifier(), highlightColor, .5f, .5f, 0f); } - if (!mechanic.HasEquippedItem("weldingtool")) + if (!mechanic.HasEquippedItem("weldingtool".ToIdentifier())) { - HighlightInventorySlot(mechanic.Inventory, "weldingtool", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "weldingtool".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; - } while (!mechanic.HasEquippedItem("divingmask") || !mechanic.HasEquippedItem("weldingtool")); // Wait until equipped + } while (!mechanic.HasEquippedItem("divingmask".ToIdentifier()) || !mechanic.HasEquippedItem("weldingtool".ToIdentifier())); // Wait until equipped SetDoorAccess(mechanic_secondDoor, mechanic_secondDoorLight, true); mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_1, mechanic_weldIcon, mechanic_repairIconColor); do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_1)); // Highlight until repaired mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_1); - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); yield return new WaitForSeconds(1f, false); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Select)); // Pump objective + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Pump objective SetHighlight(mechanic_workingPump.Item, true); do { @@ -333,9 +407,9 @@ namespace Barotrauma.Tutorials } while (mechanic_workingPump.FlowPercentage >= 0 || !mechanic_workingPump.IsActive); // Highlight until draining SetHighlight(mechanic_workingPump.Item, false); do { yield return null; } while (mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); SetDoorAccess(mechanic_thirdDoor, mechanic_thirdDoorLight, true); - //TriggerTutorialSegment(11, GameMain.Config.KeyBind(InputType.Select), GameMain.Config.KeyBind(InputType.Up), GameMain.Config.KeyBind(InputType.Down), GameMain.Config.KeyBind(InputType.Select)); // Ladder objective + //TriggerTutorialSegment(11, GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select]); // Ladder objective //do { yield return null; } while (!mechanic_ladderSensor.MotionDetected); //RemoveCompletedObjective(segments[11]); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.News"), ChatMessageType.Radio, null); @@ -362,24 +436,24 @@ namespace Barotrauma.Tutorials if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } } - if (mechanic.Inventory.FindItemByIdentifier("oxygentank") == null && mechanic.Inventory.FindItemByIdentifier("aluminium") == null) + if (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) == null && mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null) { for (int i = 0; i < mechanic_craftingCabinet.Capacity; i++) { Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); - if (item != null && item.prefab.Identifier == "oxygentank") + if (item != null && item.Prefab.Identifier == "oxygentank") { HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); } } } - if (mechanic.Inventory.FindItemByIdentifier("sodium") == null) + if (mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) == null) { for (int i = 0; i < mechanic_craftingCabinet.Inventory.Capacity; i++) { Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); - if (item != null && item.prefab.Identifier == "sodium") + if (item != null && item.Prefab.Identifier == "sodium") { HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); } @@ -387,12 +461,12 @@ namespace Barotrauma.Tutorials } } - if (!gotOxygenTank && (mechanic.Inventory.FindItemByIdentifier("oxygentank") != null || - mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") != null)) + if (!gotOxygenTank && (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null || + mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null)) { gotOxygenTank = true; } - if (!gotSodium && mechanic.Inventory.FindItemByIdentifier("sodium") != null) + if (!gotSodium && mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null) { gotSodium = true; } @@ -406,9 +480,9 @@ namespace Barotrauma.Tutorials { if (IsSelectedItem(mechanic_deconstructor.Item)) { - if (mechanic_deconstructor.OutputContainer.Inventory.FindItemByIdentifier("aluminium") != null) + if (mechanic_deconstructor.OutputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null) { - HighlightInventorySlot(mechanic_deconstructor.OutputContainer.Inventory, "aluminium", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic_deconstructor.OutputContainer.Inventory, "aluminium".ToIdentifier(), highlightColor, .5f, .5f, 0f); for (int i = 0; i < mechanic.Inventory.Capacity; i++) { @@ -417,16 +491,16 @@ namespace Barotrauma.Tutorials } else { - if (mechanic.Inventory.FindItemByIdentifier("oxygentank") != null && mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") == null) + if (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null && mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) == null) { - HighlightInventorySlot(mechanic.Inventory, "oxygentank", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "oxygentank".ToIdentifier(), highlightColor, .5f, .5f, 0f); for (int i = 0; i < mechanic_deconstructor.InputContainer.Inventory.Capacity; i++) { HighlightInventorySlot(mechanic_deconstructor.InputContainer.Inventory, i, highlightColor, .5f, .5f, 0f); } } - if (mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") != null && !mechanic_deconstructor.IsActive) + if (mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null && !mechanic_deconstructor.IsActive) { if (mechanic_deconstructor.ActivateButton.FlashTimer <= 0) { @@ -437,11 +511,11 @@ namespace Barotrauma.Tutorials } yield return null; } while ( - mechanic.Inventory.FindItemByIdentifier("aluminium") == null && - mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium") == null); // Wait until aluminium obtained + mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null && + mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null); // Wait until aluminium obtained - SetHighlight(mechanic_deconstructor.Item, false); - RemoveCompletedObjective(segments[4]); + SetHighlight(mechanic_deconstructor.Item, false); + RemoveCompletedObjective(4); yield return new WaitForSeconds(1f, false); TriggerTutorialSegment(5); // Fabricate SetHighlight(mechanic_fabricator.Item, true); @@ -455,26 +529,26 @@ namespace Barotrauma.Tutorials } else { - if (mechanic_fabricator.OutputContainer.Inventory.FindItemByIdentifier("extinguisher") != null) + if (mechanic_fabricator.OutputContainer.Inventory.FindItemByIdentifier("extinguisher".ToIdentifier()) != null) { - HighlightInventorySlot(mechanic_fabricator.OutputContainer.Inventory, "extinguisher", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic_fabricator.OutputContainer.Inventory, "extinguisher".ToIdentifier(), highlightColor, .5f, .5f, 0f); /*for (int i = 0; i < mechanic.Inventory.Capacity; i++) { if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); }*/ } - else if (mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium") != null && mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("sodium") != null && !mechanic_fabricator.IsActive) + else if (mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null && mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null && !mechanic_fabricator.IsActive) { if (mechanic_fabricator.ActivateButton.FlashTimer <= 0) { mechanic_fabricator.ActivateButton.Flash(highlightColor, 1.5f, false); } } - else if (mechanic.Inventory.FindItemByIdentifier("aluminium") != null || mechanic.Inventory.FindItemByIdentifier("sodium") != null) + else if (mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null || mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null) { - HighlightInventorySlot(mechanic.Inventory, "aluminium", highlightColor, .5f, .5f, 0f); - HighlightInventorySlot(mechanic.Inventory, "sodium", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "aluminium".ToIdentifier(), highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "sodium".ToIdentifier(), highlightColor, .5f, .5f, 0f); if (mechanic_fabricator.InputContainer.Inventory.GetItemAt(0) == null) { @@ -489,27 +563,27 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (mechanic.Inventory.FindItemByIdentifier("extinguisher") == null); // Wait until extinguisher is created - RemoveCompletedObjective(segments[5]); + } while (mechanic.Inventory.FindItemByIdentifier("extinguisher".ToIdentifier()) == null); // Wait until extinguisher is created + RemoveCompletedObjective(5); SetHighlight(mechanic_fabricator.Item, false); SetDoorAccess(mechanic_fourthDoor, mechanic_fourthDoorLight, true); // Room 5 do { yield return null; } while (!mechanic_fireSensor.MotionDetected); - TriggerTutorialSegment(6, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot)); // Using the extinguisher + TriggerTutorialSegment(6, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Using the extinguisher do { yield return null; } while (!mechanic_fire.Removed); // Wait until extinguished yield return new WaitForSeconds(3f, false); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); - if (mechanic.HasEquippedItem("extinguisher")) // do not trigger if dropped already + if (mechanic.HasEquippedItem("extinguisher".ToIdentifier())) // do not trigger if dropped already { TriggerTutorialSegment(7); do { - HighlightInventorySlot(mechanic.Inventory, "extinguisher", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "extinguisher".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); yield return null; - } while (mechanic.HasEquippedItem("extinguisher")); - RemoveCompletedObjective(segments[7]); + } while (mechanic.HasEquippedItem("extinguisher".ToIdentifier())); + RemoveCompletedObjective(7); } SetDoorAccess(mechanic_fifthDoor, mechanic_fifthDoorLight, true); @@ -531,9 +605,9 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (!mechanic.HasEquippedItem("divingsuit", slotType: InvSlotType.OuterClothes)); + } while (!mechanic.HasEquippedItem("divingsuit".ToIdentifier(), slotType: InvSlotType.OuterClothes)); SetHighlight(mechanic_divingSuitContainer.Item, false); - RemoveCompletedObjective(segments[8]); + RemoveCompletedObjective(8); SetDoorAccess(tutorial_mechanicFinalDoor, tutorial_mechanicFinalDoorLight, true); // Room 7 @@ -542,7 +616,7 @@ namespace Barotrauma.Tutorials mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_2); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(9, GameMain.Config.KeyBindText(InputType.Use)); // Repairing machinery (pump) + TriggerTutorialSegment(9, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)); // Repairing machinery (pump) SetHighlight(mechanic_brokenPump.Item, true); mechanic_brokenPump.CanBeSelected = true; Repairable repairablePumpComponent = mechanic_brokenPump.Item.GetComponent(); @@ -552,9 +626,9 @@ namespace Barotrauma.Tutorials yield return null; if (repairablePumpComponent.IsBelowRepairThreshold) { - if (!mechanic.HasEquippedItem("wrench")) + if (!mechanic.HasEquippedItem("wrench".ToIdentifier())) { - HighlightInventorySlot(mechanic.Inventory, "wrench", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "wrench".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } else if (IsSelectedItem(mechanic_brokenPump.Item) && repairablePumpComponent.CurrentFixer == null) { @@ -575,7 +649,7 @@ namespace Barotrauma.Tutorials } } } while (repairablePumpComponent.IsBelowRepairThreshold || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); - RemoveCompletedObjective(segments[9]); + RemoveCompletedObjective(9); SetHighlight(mechanic_brokenPump.Item, false); do { yield return null; } while (mechanic_brokenhull_2.WaterPercentage > waterVolumeBeforeOpening); SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); @@ -599,7 +673,7 @@ namespace Barotrauma.Tutorials // Remove highlights when each individual machine is repaired do { CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); yield return null; } while (repairablePumpComponent1.IsBelowRepairThreshold || repairablePumpComponent2.IsBelowRepairThreshold || repairableEngineComponent.IsBelowRepairThreshold); CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); - RemoveCompletedObjective(segments[10]); + RemoveCompletedObjective(10); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Complete"), ChatMessageType.Radio, null); // END TUTORIAL diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 594e847cb..68d91df57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -72,62 +72,114 @@ namespace Barotrauma.Tutorials private PowerContainer officer_subSuperCapacitor_2; // Variables - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character officer; private float superCapacitorRechargeRate = 10; private Sprite officer_gunIcon; private Color officer_gunIconColor; - public OfficerTutorial(XElement element) : base(element) + public OfficerTutorial() : base("tutorial.securityofficertraining".ToIdentifier(), + new Segment( + "Mechanic.Equipment".ToIdentifier(), + "Mechanic.EquipmentObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.MeleeWeapon".ToIdentifier(), + "Officer.MeleeWeaponObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.MeleeWeaponText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.Crawler".ToIdentifier(), + "Officer.CrawlerObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.CrawlerText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.SomethingBig".ToIdentifier(), + "Officer.SomethingBigObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Officer.SomethingBigText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_loaders.webm", TextTag = "Officer.SomethingBigText".ToIdentifier(), Width = 700, Height = 80 }), + new Segment( + "Officer.Hammerhead".ToIdentifier(), + "Officer.HammerheadObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.HammerheadText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.RangedWeapon".ToIdentifier(), + "Officer.RangedWeaponObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Officer.RangedWeaponText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_ranged.webm", TextTag = "Officer.RangedWeaponText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Officer.Mudraptor".ToIdentifier(), + "Officer.MudraptorObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.MudraptorText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.ArmSubmarine".ToIdentifier(), + "Officer.ArmSubmarineObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.ArmSubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) + { } + + protected override CharacterInfo GetCharacterInfo() { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 20), + new Skill("weapons".ToIdentifier(), 70), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 20))); } - public override void Start() + protected override void Initialize() { - base.Start(); - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); officer = Character.Controlled; - var handcuffs = FindOrGiveItem(officer, "handcuffs"); + var handcuffs = FindOrGiveItem(officer, "handcuffs".ToIdentifier()); handcuffs.Unequip(officer); officer.Inventory.RemoveItem(handcuffs); - var stunbaton = FindOrGiveItem(officer, "stunbaton"); + var stunbaton = FindOrGiveItem(officer, "stunbaton".ToIdentifier()); stunbaton.Unequip(officer); officer.Inventory.RemoveItem(stunbaton); - var smg = FindOrGiveItem(officer, "smg"); + var smg = FindOrGiveItem(officer, "smg".ToIdentifier()); smg.Unequip(officer); officer.Inventory.RemoveItem(smg); - var divingknife = FindOrGiveItem(officer, "divingknife"); + var divingknife = FindOrGiveItem(officer, "divingknife".ToIdentifier()); divingknife.Unequip(officer); officer.Inventory.RemoveItem(divingknife); - var steroids = FindOrGiveItem(officer, "steroids"); + var steroids = FindOrGiveItem(officer, "steroids".ToIdentifier()); steroids.Unequip(officer); officer.Inventory.RemoveItem(steroids); var ballistichelmet = - officer.Inventory.FindItemByIdentifier("ballistichelmet1") ?? - officer.Inventory.FindItemByIdentifier("ballistichelmet2") ?? - FindOrGiveItem(officer, "ballistichelmet3"); + officer.Inventory.FindItemByIdentifier("ballistichelmet1".ToIdentifier()) ?? + officer.Inventory.FindItemByIdentifier("ballistichelmet2".ToIdentifier()) ?? + FindOrGiveItem(officer, "ballistichelmet3".ToIdentifier()); ballistichelmet.Unequip(officer); officer.Inventory.RemoveItem(ballistichelmet); - var bodyarmor = FindOrGiveItem(officer, "bodyarmor"); + var bodyarmor = FindOrGiveItem(officer, "bodyarmor".ToIdentifier()); bodyarmor.Unequip(officer); officer.Inventory.RemoveItem(bodyarmor); - var gunOrder = Order.GetPrefab("operateweapons"); + var gunOrder = OrderPrefab.Prefabs["operateweapons"]; officer_gunIcon = gunOrder.SymbolSprite; officer_gunIconColor = gunOrder.Color; - var bandage = FindOrGiveItem(officer, "antibleeding1"); + var bandage = FindOrGiveItem(officer, "antibleeding1".ToIdentifier()); bandage.Unequip(officer); officer.Inventory.RemoveItem(bandage); - FindOrGiveItem(officer, "antibleeding1"); + FindOrGiveItem(officer, "antibleeding1".ToIdentifier()); // Other tutorial items tutorial_mechanicFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoorlight")).GetComponent(); @@ -222,7 +274,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!officer_equipmentObjectiveSensor.MotionDetected); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Equipment"), ChatMessageType.Radio, null); yield return new WaitForSeconds(3f, false); - //TriggerTutorialSegment(0, GameMain.Config.KeyBind(InputType.Select), GameMain.Config.KeyBind(InputType.Deselect)); // Retrieve equipment + //TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Deselect]); // Retrieve equipment SetHighlight(officer_equipmentCabinet.Item, true); bool firstSlotRemoved = false; bool secondSlotRemoved = false; @@ -260,24 +312,24 @@ namespace Barotrauma.Tutorials //RemoveCompletedObjective(segments[0]); SetHighlight(officer_equipmentCabinet.Item, false); do { yield return null; } while (IsSelectedItem(officer_equipmentCabinet.Item)); - TriggerTutorialSegment(1, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot)); // Equip melee weapon & armor + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Equip melee weapon & armor do { - if (!officer.HasEquippedItem("stunbaton")) + if (!officer.HasEquippedItem("stunbaton".ToIdentifier())) { - HighlightInventorySlot(officer.Inventory, "stunbaton", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "stunbaton".ToIdentifier(), highlightColor, .5f, .5f, 0f); } - if (!officer.HasEquippedItem("bodyarmor")) + if (!officer.HasEquippedItem("bodyarmor".ToIdentifier())) { - HighlightInventorySlot(officer.Inventory, "bodyarmor", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "bodyarmor".ToIdentifier(), highlightColor, .5f, .5f, 0f); } - if (!officer.HasEquippedItem("ballistichelmet1")) + if (!officer.HasEquippedItem("ballistichelmet1".ToIdentifier())) { - HighlightInventorySlot(officer.Inventory, "ballistichelmet1", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "ballistichelmet1".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return new WaitForSeconds(1f, false); - } while (!officer.HasEquippedItem("stunbaton") || !officer.HasEquippedItem("bodyarmor") || !officer.HasEquippedItem("ballistichelmet1")); - RemoveCompletedObjective(segments[1]); + } while (!officer.HasEquippedItem("stunbaton".ToIdentifier()) || !officer.HasEquippedItem("bodyarmor".ToIdentifier()) || !officer.HasEquippedItem("ballistichelmet1".ToIdentifier())); + RemoveCompletedObjective(1); SetDoorAccess(officer_firstDoor, officer_firstDoorLight, true); // Room 3 @@ -285,7 +337,7 @@ namespace Barotrauma.Tutorials TriggerTutorialSegment(2); officer_crawler = SpawnMonster("crawler", officer_crawlerSpawnPos); do { yield return null; } while (!officer_crawler.IsDead); - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); Heal(officer); yield return new WaitForSeconds(1f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.CrawlerDead"), ChatMessageType.Radio, null); @@ -305,7 +357,7 @@ namespace Barotrauma.Tutorials SetHighlight(officer_ammoShelf_2.Item, officer_coilgunLoader.Item.ExternalHighlight ); if (IsSelectedItem(officer_coilgunLoader.Item)) { - HighlightInventorySlot(officer.Inventory, "coilgunammobox", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "coilgunammobox".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; } while (officer_coilgunLoader.Inventory.GetItemAt(0) == null || officer_superCapacitor.RechargeSpeed < superCapacitorRechargeRate || officer_coilgunLoader.Inventory.GetItemAt(0).Condition == 0); @@ -313,9 +365,9 @@ namespace Barotrauma.Tutorials SetHighlight(officer_superCapacitor.Item, false); SetHighlight(officer_ammoShelf_1.Item, false); SetHighlight(officer_ammoShelf_2.Item, false); - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect)); // Kill hammerhead + TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Kill hammerhead officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); officer_hammerhead.Params.AI.AvoidAbyss = false; officer_hammerhead.Params.AI.StayInAbyss = false; @@ -348,7 +400,7 @@ namespace Barotrauma.Tutorials while(!officer_hammerhead.IsDead); Heal(officer); SetHighlight(officer_coilgunPeriscope, false); - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); yield return new WaitForSeconds(1f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.HammerheadDead"), ChatMessageType.Radio, null); SetDoorAccess(officer_thirdDoor, officer_thirdDoorLight, true); @@ -357,16 +409,16 @@ namespace Barotrauma.Tutorials //do { yield return null; } while (!officer_rangedWeaponSensor.MotionDetected); do { yield return null; } while (!officer_thirdDoor.IsOpen); yield return new WaitForSeconds(3f, false); - TriggerTutorialSegment(5, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot)); // Ranged weapons + TriggerTutorialSegment(5, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Ranged weapons SetHighlight(officer_rangedWeaponHolder.Item, true); do { yield return null; } while (!officer_rangedWeaponHolder.Inventory.IsEmpty()); // Wait until looted SetHighlight(officer_rangedWeaponHolder.Item, false); do { - HighlightInventorySlot(officer.Inventory, "shotgun", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(officer.Inventory, "shotgun".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); yield return null; - } while (!officer.HasEquippedItem("shotgun")); // Wait until equipped - ItemContainer shotGunChamber = officer.Inventory.FindItemByIdentifier("shotgun").GetComponent(); + } while (!officer.HasEquippedItem("shotgun".ToIdentifier())); // Wait until equipped + ItemContainer shotGunChamber = officer.Inventory.FindItemByIdentifier("shotgun".ToIdentifier()).GetComponent(); SetHighlight(officer_rangedWeaponCabinet.Item, true); do { @@ -392,13 +444,13 @@ namespace Barotrauma.Tutorials } } - if (officer.Inventory.FindItemByIdentifier("shotgunshell") != null || (IsSelectedItem(officer_rangedWeaponCabinet.Item) && officer_rangedWeaponCabinet.Inventory.FindItemByIdentifier("shotgunshell") != null)) + if (officer.Inventory.FindItemByIdentifier("shotgunshell".ToIdentifier()) != null || (IsSelectedItem(officer_rangedWeaponCabinet.Item) && officer_rangedWeaponCabinet.Inventory.FindItemByIdentifier("shotgunshell".ToIdentifier()) != null)) { - HighlightInventorySlot(officer.Inventory, "shotgun", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(officer.Inventory, "shotgun".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } yield return null; } while (!shotGunChamber.Inventory.IsFull(takeStacksIntoAccount: true)); // Wait until all six harpoons loaded - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); SetHighlight(officer_rangedWeaponCabinet.Item, false); SetDoorAccess(officer_fourthDoor, officer_fourthDoorLight, true); @@ -408,7 +460,7 @@ namespace Barotrauma.Tutorials officer_mudraptor = SpawnMonster("mudraptor", officer_mudraptorSpawnPos); do { yield return null; } while (!officer_mudraptor.IsDead); Heal(officer); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); SetDoorAccess(tutorial_securityFinalDoor, tutorial_securityFinalDoorLight, true); // Submarine @@ -459,7 +511,7 @@ namespace Barotrauma.Tutorials officer.RemoveActiveObjectiveEntity(officer_subSuperCapacitor_2.Item); officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_1); officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_2); - RemoveCompletedObjective(segments[7]); + RemoveCompletedObjective(7); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Complete"), ChatMessageType.Radio, null); yield return new WaitForSeconds(4f, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 4cb49e6fb..60369b132 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -8,18 +8,21 @@ using System.Xml.Linq; namespace Barotrauma.Tutorials { - class ScenarioTutorial : Tutorial + abstract class ScenarioTutorial : Tutorial { private CoroutineHandle tutorialCoroutine; private Character character; - private string spawnSub; - private SpawnType spawnPointType; - private string submarinePath; - private string startOutpostPath; - private string endOutpostPath; - private string levelSeed; - private string levelParams; + + private const string submarinePath = "Content/Tutorials/Dugong_Tutorial.sub"; + private const string startOutpostPath = "Content/Tutorials/TutorialOutpost.sub"; + //private const string endOutpostPath = ""; + + private const string levelSeed = "nLoZLLtza"; + private const string levelParams = "ColdCavernsTutorial"; + + //private const string spawnSub = "startoutpost"; + private const SpawnType spawnPointType = SpawnType.Human; private SubmarineInfo startOutpost = null; private SubmarineInfo endOutpost = null; @@ -31,34 +34,18 @@ namespace Barotrauma.Tutorials protected Color highlightColor = Color.OrangeRed; protected Color uiHighlightColor = new Color(150, 50, 0); protected Color buttonHighlightColor = new Color(255, 100, 0); - protected Color inaccessibleColor = GUI.Style.Red; - protected Color accessibleColor = GUI.Style.Green; + protected Color inaccessibleColor = GUIStyle.Red; + protected Color accessibleColor = GUIStyle.Green; - public ScenarioTutorial(XElement element) : base(element) - { - submarinePath = element.GetAttributeString("submarinepath", ""); - startOutpostPath = element.GetAttributeString("startoutpostpath", ""); - endOutpostPath = element.GetAttributeString("endoutpostpath", ""); + protected ScenarioTutorial(Identifier identifier, params Segment[] segments) : base(identifier, segments) { } - levelSeed = element.GetAttributeString("levelseed", "tuto"); - levelParams = element.GetAttributeString("levelparams", ""); - - spawnSub = element.GetAttributeString("spawnsub", ""); - Enum.TryParse(element.GetAttributeString("spawnpointtype", "Human"), true, out spawnPointType); - } - - public override void Initialize() - { - base.Initialize(); - currentTutorialCompleted = false; - GameMain.Instance.ShowLoading(Loading()); - } - - private IEnumerable Loading() + protected abstract void Initialize(); + + protected override IEnumerable Loading() { SubmarineInfo subInfo = new SubmarineInfo(submarinePath); - LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier.Equals(levelParams, StringComparison.OrdinalIgnoreCase)); + LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier == levelParams); yield return CoroutineStatus.Running; @@ -68,18 +55,18 @@ namespace Barotrauma.Tutorials if (generationParams != null) { Biome biome = - LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? - LevelGenerationParams.GetBiomes().First(); + Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ?? + Biome.Prefabs.First(); if (!string.IsNullOrEmpty(startOutpostPath)) { startOutpost = new SubmarineInfo(startOutpostPath); } - if (!string.IsNullOrEmpty(endOutpostPath)) + /*if (!string.IsNullOrEmpty(endOutpostPath)) { endOutpost = new SubmarineInfo(endOutpostPath); - } + }*/ LevelData tutorialLevel = new LevelData(levelSeed, 0, 0, generationParams, biome); GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost, endOutpost: endOutpost); @@ -93,12 +80,6 @@ namespace Barotrauma.Tutorials GameMain.GameSession.EventManager.Enabled = false; GameMain.GameScreen.Select(); - yield return CoroutineStatus.Success; - } - - public override void Start() - { - base.Start(); Submarine.MainSub.GodMode = true; foreach (Structure wall in Structure.WallList) @@ -109,16 +90,15 @@ namespace Barotrauma.Tutorials } } - CharacterInfo charInfo = configElement.Element("Character") == null ? - new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")) : - new CharacterInfo(configElement.Element("Character")); + CharacterInfo charInfo = GetCharacterInfo(); WayPoint wayPoint = GetSpawnPoint(charInfo); if (wayPoint == null) { DebugConsole.ThrowError("A waypoint with the spawntype \"" + spawnPointType + "\" is required for the tutorial event"); - return; + yield return CoroutineStatus.Failure; + yield break; } character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false); @@ -126,11 +106,12 @@ namespace Barotrauma.Tutorials Character.Controlled = character; character.GiveJobItems(null); - var idCard = character.Inventory.FindItemByIdentifier("idcard"); + var idCard = character.Inventory.FindItemByIdentifier("idcard".ToIdentifier()); if (idCard == null) { DebugConsole.ThrowError("Item prefab \"ID Card\" not found!"); - return; + yield return CoroutineStatus.Failure; + yield break; } idCard.AddTag("com"); idCard.AddTag("eng"); @@ -145,8 +126,14 @@ namespace Barotrauma.Tutorials } tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); + + Initialize(); + + yield return CoroutineStatus.Success; } + protected abstract CharacterInfo GetCharacterInfo(); + public override void AddToGUIUpdateList() { if (!currentTutorialCompleted) @@ -157,7 +144,7 @@ namespace Barotrauma.Tutorials private WayPoint GetSpawnPoint(CharacterInfo charInfo) { - Submarine spawnSub = null; + /*Submarine spawnSub = null; if (this.spawnSub != string.Empty) { @@ -175,15 +162,15 @@ namespace Barotrauma.Tutorials spawnSub = Submarine.MainSub; break; } - } - + }*/ + Submarine spawnSub = Level.Loaded.StartOutpost; return WayPoint.GetRandom(spawnPointType, charInfo.Job?.Prefab, spawnSub); } protected bool HasOrder(Character character, string identifier, string option = null) { var currentOrderInfo = character.GetCurrentOrderWithTopPriority(); - if (currentOrderInfo?.Order?.Identifier == identifier) + if (currentOrderInfo?.Identifier == identifier) { if (option == null) { @@ -191,7 +178,7 @@ namespace Barotrauma.Tutorials } else { - return currentOrderInfo?.OrderOption == option; + return currentOrderInfo?.Option == option; } } @@ -267,7 +254,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(3.0f); - var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); messageBox.Buttons[0].OnClicked += Restart; messageBox.Buttons[0].OnClicked += messageBox.Close; @@ -303,7 +290,7 @@ namespace Barotrauma.Tutorials character.SetStun(0.0f, true); } - protected Item FindOrGiveItem(Character character, string identifier) + protected Item FindOrGiveItem(Character character, Identifier identifier) { var item = character.Inventory.FindItemByIdentifier(identifier); if (item != null && !item.Removed) { return item; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 87f3c404e..42795e0bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -7,189 +7,128 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma.Tutorials { + enum TutorialContentType { None = 0, Video = 1, ManualVideo = 2, TextOnly = 3 }; + + /// + /// If you're seeing this and are currently working on improving the tutorials, consider + /// deleting this class and all that derive from it, and starting from scratch. + /// abstract class Tutorial { - #region Tutorial variables - public static bool Initialized = false; - public static bool ContentRunning = false; - public static List Tutorials; + #region Constants + public const string PlayableContentPath = "Content/Tutorials/TutorialVideos/"; + #endregion + + #region Tutorial variables + public static ImmutableHashSet Types; + static Tutorial() + { + Types = ReflectionUtils.GetDerivedNonAbstract() + .ToImmutableHashSet(); + } + + public readonly Identifier Identifier; + + public LocalizedString DisplayName { get; } + + public bool ContentRunning { get; protected set; } - protected bool started = false; protected GUIComponent infoBox; private Action infoBoxClosedCallback; - protected XElement configElement; protected VideoPlayer videoPlayer; - protected enum TutorialContentTypes { None = 0, Video = 1, ManualVideo = 2, TextOnly = 3 }; - protected string playableContentPath; protected Point screenResolution; protected WindowMode windowMode; protected float prevUIScale; private GUIFrame holderFrame, objectiveFrame; - private List activeObjectives = new List(); - private string objectiveTranslated; + private readonly List activeObjectives; + private readonly LocalizedString objectiveTranslated; - protected TutorialSegment activeContentSegment; - protected List segments; + protected readonly ImmutableArray segments; + protected Index activeContentSegmentIndex; + protected Segment activeContentSegment => segments[activeContentSegmentIndex]; - protected class TutorialSegment + protected class Segment { - public string Id; - public string Objective; - public TutorialContentTypes ContentType; - public XElement TextContent; - public XElement VideoContent; + public struct Text + { + public Identifier Tag; + public int Width; + public int Height; + public Anchor Anchor; + } + + public struct Video + { + public string File; + public Identifier TextTag; + public int Width; + public int Height; + } + public bool IsTriggered; public GUIButton ReplayButton; public GUITextBlock LinkedTitle, LinkedText; public object[] Args; + public LocalizedString Objective; - public TutorialSegment(XElement config) + public readonly Identifier Id; + public readonly Text? TextContent; + public readonly Video? VideoContent; + public readonly TutorialContentType ContentType; + + public Segment(Identifier id, Identifier objectiveTextTag, TutorialContentType contentType, Text? textContent = null, Video? videoContent = null) { - Id = config.GetAttributeString("id", "Missing ID"); - Objective = TextManager.Get(config.GetAttributeString("objective", string.Empty), true); - Enum.TryParse(config.GetAttributeString("contenttype", "None"), true, out ContentType); - IsTriggered = config.GetAttributeBool("istriggered", false); + Id = id; + Objective = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + ContentType = contentType; + TextContent = textContent; + VideoContent = videoContent; - switch (ContentType) - { - case TutorialContentTypes.None: - break; - case TutorialContentTypes.Video: - case TutorialContentTypes.ManualVideo: - VideoContent = config.Element("Video"); - TextContent = config.Element("Text"); - break; - case TutorialContentTypes.TextOnly: - TextContent = config.Element("Text"); - break; - } + IsTriggered = false; } } - public string Identifier - { - get; - protected set; - } - - public string DisplayName - { - get; - protected set; - } - private bool completed; public bool Completed { get { return completed; } protected set { - if (completed == value) return; + if (completed == value) { return; } completed = value; - GameMain.Config.SaveNewPlayerConfig(); + if (value) + { + CompletedTutorials.Instance.Add(Identifier); + } + GameSettings.SaveCurrentConfig(); } } #endregion #region Tutorial Controls - public static void Init() + protected Tutorial(Identifier identifier, params Segment[] segments) { - Tutorials = new List(); - foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.Tutorials)) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) - { - Tutorial newTutorial = Load(element); - if (newTutorial != null) Tutorials.Add(newTutorial); - } - } - } - - private static Tutorial Load(XElement element) - { - Type t; - string type = element.Name.ToString().ToLowerInvariant(); - try - { - // Get the type of a specified class. - t = Type.GetType("Barotrauma.Tutorials." + type + "", false, true); - if (t == null) - { - DebugConsole.ThrowError("Could not find tutorial type \"" + type + "\""); - return null; - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Could not find tutorial type \"" + type + "\"", e); - return null; - } - - ConstructorInfo constructor; - try - { - if (!t.IsSubclassOf(typeof(Tutorial))) return null; - constructor = t.GetConstructor(new Type[] { typeof(XElement) }); - if (constructor == null) - { - DebugConsole.ThrowError("Could not find the constructor of tutorial type \"" + type + "\""); - return null; - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Could not find the constructor of tutorial type \"" + type + "\"", e); - return null; - } - Tutorial tutorial = null; - try - { - object component = constructor.Invoke(new object[] { element }); - tutorial = (Tutorial)component; - } - catch (TargetInvocationException e) - { - DebugConsole.ThrowError("Error while loading tutorial of the type " + t + ".", e.InnerException); - } - - return tutorial; - } - - public Tutorial(XElement element) - { - configElement = element; - Identifier = element.GetAttributeString("identifier", "unknown"); + Identifier = identifier; + this.segments = segments.ToImmutableArray(); DisplayName = TextManager.Get(Identifier); - completed = GameMain.Config.CompletedTutorialNames.Contains(Identifier); - playableContentPath = element.GetAttributeString("playablecontentpath", ""); - - segments = new List(); - - foreach (var segment in element.Elements("Segment")) - { - segments.Add(new TutorialSegment(segment)); - } - } - - public virtual void Initialize() - { - if (Initialized) return; - Initialized = true; - videoPlayer = new VideoPlayer(); - } - - public virtual void Start() - { - activeObjectives.Clear(); + activeObjectives = new List(); objectiveTranslated = TextManager.Get("Tutorial.Objective"); + } + + protected abstract IEnumerable Loading(); + + public void Start() + { + videoPlayer = new VideoPlayer(); + GameMain.Instance.ShowLoading(Loading()); + + activeObjectives.Clear(); CreateObjectiveFrame(); // Setup doors: Clear all requirements, unless the door is setup as locked. @@ -208,7 +147,7 @@ namespace Barotrauma.Tutorials public virtual void AddToGUIUpdateList() { - if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameMain.Config.WindowMode != windowMode) + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameSettings.CurrentConfig.Graphics.DisplayMode != windowMode) { CreateObjectiveFrame(); } @@ -255,74 +194,69 @@ namespace Barotrauma.Tutorials protected bool Restart(GUIButton button, object obj) { GUI.PreventPauseMenuToggle = false; - TutorialMode.StartTutorial(this); return true; } - protected virtual void TriggerTutorialSegment(int index, params object[] args) + protected virtual void TriggerTutorialSegment(Index index, params object[] args) { Inventory.DraggingItems.Clear(); ContentRunning = true; - activeContentSegment = segments[index]; + activeContentSegmentIndex = index; segments[index].Args = args; - string tutorialText = TextManager.GetFormatted(activeContentSegment.TextContent.GetAttributeString("tag", ""), true, args); + LocalizedString tutorialText = TextManager.GetFormatted(segments[index].TextContent.Value.Tag, args); tutorialText = TextManager.ParseInputTypes(tutorialText); - string objectiveText = string.Empty; + LocalizedString objectiveText = string.Empty; - if (!string.IsNullOrEmpty(activeContentSegment.Objective)) + if (!segments[index].Objective.IsNullOrEmpty()) { if (args.Length == 0) { - objectiveText = activeContentSegment.Objective; + objectiveText = segments[index].Objective; } else { - objectiveText = string.Format(activeContentSegment.Objective, args); + objectiveText = TextManager.GetFormatted(segments[index].Objective, args); } objectiveText = TextManager.ParseInputTypes(objectiveText); - activeContentSegment.Objective = objectiveText; + segments[index].Objective = objectiveText; } else { - activeContentSegment.IsTriggered = true; // Complete at this stage only if no related objective + segments[index].IsTriggered = true; // Complete at this stage only if no related objective } - switch (activeContentSegment.ContentType) + switch (segments[index].ContentType) { - case TutorialContentTypes.None: + case TutorialContentType.None: break; - case TutorialContentTypes.Video: + case TutorialContentType.Video: infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.GetAttributeInt("width", 300), - activeContentSegment.TextContent.GetAttributeInt("height", 80), - activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, () => LoadVideo(activeContentSegment)); + activeContentSegment.TextContent.Value.Width, + activeContentSegment.TextContent.Value.Height, + activeContentSegment.TextContent.Value.Anchor, true, () => LoadVideo(activeContentSegment)); break; - case TutorialContentTypes.ManualVideo: + case TutorialContentType.ManualVideo: infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.GetAttributeInt("width", 300), - activeContentSegment.TextContent.GetAttributeInt("height", 80), - activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, StopCurrentContentSegment, () => LoadVideo(activeContentSegment)); + activeContentSegment.TextContent.Value.Width, + activeContentSegment.TextContent.Value.Height, + activeContentSegment.TextContent.Value.Anchor, true, StopCurrentContentSegment, () => LoadVideo(activeContentSegment)); break; - case TutorialContentTypes.TextOnly: + case TutorialContentType.TextOnly: infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.GetAttributeInt("width", 300), - activeContentSegment.TextContent.GetAttributeInt("height", 80), - activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, StopCurrentContentSegment); + activeContentSegment.TextContent.Value.Width, + activeContentSegment.TextContent.Value.Height, + activeContentSegment.TextContent.Value.Anchor, true, StopCurrentContentSegment); break; } } public virtual void Stop() { - started = ContentRunning = Initialized = false; + ContentRunning = false; infoBox = null; - if (videoPlayer != null) - { - videoPlayer.Remove(); - videoPlayer = null; - } + videoPlayer.Remove(); } #endregion @@ -334,50 +268,51 @@ namespace Barotrauma.Tutorials for (int i = 0; i < activeObjectives.Count; i++) { - CreateObjectiveGUI(activeObjectives[i], i, activeObjectives[i].ContentType); + CreateObjectiveGUI(activeObjectives[i], i, segments[activeObjectives[i]].ContentType); } screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - windowMode = GameMain.Config.WindowMode; + windowMode = GameSettings.CurrentConfig.Graphics.DisplayMode; prevUIScale = GUI.Scale; } protected void StopCurrentContentSegment() { - if (!string.IsNullOrEmpty(activeContentSegment.Objective)) + if (!activeContentSegment.Objective.IsNullOrEmpty()) { - AddNewObjective(activeContentSegment, activeContentSegment.ContentType); + AddNewObjective(activeContentSegmentIndex, activeContentSegment.ContentType); } - activeContentSegment = null; ContentRunning = false; + activeContentSegmentIndex = Index.End; } - protected virtual void CheckActiveObjectives(TutorialSegment objective, float deltaTime) + protected virtual void CheckActiveObjectives(Index objective, float deltaTime) { } - protected bool HasObjective(TutorialSegment segment) + protected bool HasObjective(Index segment) { return activeObjectives.Contains(segment); } - protected void AddNewObjective(TutorialSegment segment, TutorialContentTypes type) + protected void AddNewObjective(Index segment, TutorialContentType type) { activeObjectives.Add(segment); CreateObjectiveGUI(segment, activeObjectives.Count - 1, type); } - private void CreateObjectiveGUI(TutorialSegment segment, int index, TutorialContentTypes type) + private void CreateObjectiveGUI(Index segmentIndex, int index, TutorialContentType type) { - string objectiveText = TextManager.ParseInputTypes(segment.Objective); - Point replayButtonSize = new Point((int)(GUI.LargeFont.MeasureString(objectiveText).X), (int)(GUI.LargeFont.MeasureString(objectiveText).Y * 1.45f)); + var segment = segments[segmentIndex]; + LocalizedString objectiveText = TextManager.ParseInputTypes(segment.Objective); + Point replayButtonSize = new Point((int)(GUIStyle.LargeFont.MeasureString(objectiveText).X), (int)(GUIStyle.LargeFont.MeasureString(objectiveText).Y * 1.45f)); segment.ReplayButton = new GUIButton(new RectTransform(replayButtonSize, objectiveFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft) { AbsoluteOffset = new Point(0, (replayButtonSize.Y + (int)(20f * GUI.Scale)) * index) }, style: null); segment.ReplayButton.OnClicked += (GUIButton btn, object userdata) => { - if (type == TutorialContentTypes.Video) + if (type == TutorialContentType.Video) { ReplaySegmentVideo(segment); } @@ -388,23 +323,23 @@ namespace Barotrauma.Tutorials return true; }; - string objectiveTitleText = TextManager.ParseInputTypes(objectiveTranslated); - int yOffset = (int)((GUI.SubHeadingFont.MeasureString(objectiveTitleText).Y + 5)); - segment.LinkedTitle = new GUITextBlock(new RectTransform(new Point((int)GUI.SubHeadingFont.MeasureString(objectiveTitleText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.BottomLeft) /*{ AbsoluteOffset = new Point((int)(-10 * GUI.Scale), 0) }*/, - objectiveTitleText, textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + LocalizedString objectiveTitleText = TextManager.ParseInputTypes(objectiveTranslated); + int yOffset = (int)((GUIStyle.SubHeadingFont.MeasureString(objectiveTitleText).Y + 5)); + segment.LinkedTitle = new GUITextBlock(new RectTransform(new Point((int)GUIStyle.SubHeadingFont.MeasureString(objectiveTitleText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.BottomLeft) /*{ AbsoluteOffset = new Point((int)(-10 * GUI.Scale), 0) }*/, + objectiveTitleText, textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - segment.LinkedText = new GUITextBlock(new RectTransform(new Point((int)GUI.LargeFont.MeasureString(objectiveText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.TopLeft) /*{ AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }*/, - objectiveText, textColor: new Color(4, 180, 108), font: GUI.LargeFont, textAlignment: Alignment.CenterLeft); + segment.LinkedText = new GUITextBlock(new RectTransform(new Point((int)GUIStyle.LargeFont.MeasureString(objectiveText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.TopLeft) /*{ AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }*/, + objectiveText, textColor: new Color(4, 180, 108), font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft); segment.LinkedTitle.Color = segment.LinkedTitle.HoverColor = segment.LinkedTitle.PressedColor = segment.LinkedTitle.SelectedColor = Color.Transparent; segment.LinkedText.Color = segment.LinkedText.HoverColor = segment.LinkedText.PressedColor = segment.LinkedText.SelectedColor = Color.Transparent; segment.ReplayButton.Color = segment.ReplayButton.HoverColor = segment.ReplayButton.PressedColor = segment.ReplayButton.SelectedColor = Color.Transparent; } - private void ReplaySegmentVideo(TutorialSegment segment) + private void ReplaySegmentVideo(Segment segment) { if (ContentRunning) return; Inventory.DraggingItems.Clear(); @@ -413,30 +348,31 @@ namespace Barotrauma.Tutorials //videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, callback: () => ContentRunning = false); } - private void ShowSegmentText(TutorialSegment segment) + private void ShowSegmentText(Segment segment) { if (ContentRunning) return; Inventory.DraggingItems.Clear(); ContentRunning = true; - string tutorialText = TextManager.GetFormatted(segment.TextContent.GetAttributeString("tag", ""), true, segment.Args); + LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Value.Tag, segment.Args); Action videoAction = null; - if (segment.ContentType != TutorialContentTypes.TextOnly) + if (segment.ContentType != TutorialContentType.TextOnly) { videoAction = () => LoadVideo(segment); } infoBox = CreateInfoFrame(TextManager.Get(segment.Id), tutorialText, - segment.TextContent.GetAttributeInt("width", 300), - segment.TextContent.GetAttributeInt("height", 80), - segment.TextContent.GetAttributeString("anchor", "Center"), true, () => ContentRunning = false, videoAction); + segment.TextContent.Value.Width, + segment.TextContent.Value.Height, + segment.TextContent.Value.Anchor, true, () => ContentRunning = false, videoAction); } - protected void RemoveCompletedObjective(TutorialSegment segment) + protected void RemoveCompletedObjective(Index segmentIndex) { - if (!HasObjective(segment)) return; + if (!HasObjective(segmentIndex)) return; + var segment = segments[segmentIndex]; segment.IsTriggered = true; segment.ReplayButton.OnClicked = null; @@ -467,18 +403,20 @@ namespace Barotrauma.Tutorials GUIImage stroke = new GUIImage(rectTB, "Stroke"); stroke.Color = stroke.SelectedColor = stroke.HoverColor = stroke.PressedColor = color; - CoroutineManager.StartCoroutine(WaitForObjectiveEnd(segment)); + CoroutineManager.StartCoroutine(WaitForObjectiveEnd(segmentIndex)); } - private IEnumerable WaitForObjectiveEnd(TutorialSegment objective) + private IEnumerable WaitForObjectiveEnd(Index objectiveIndex) { + var objective = segments[objectiveIndex]; yield return new WaitForSeconds(2.0f); objectiveFrame.RemoveChild(objective.ReplayButton); - activeObjectives.Remove(objective); + activeObjectives.Remove(objectiveIndex); for (int i = 0; i < activeObjectives.Count; i++) { - activeObjectives[i].ReplayButton.RectTransform.AbsoluteOffset = new Point(0, (activeObjectives[i].ReplayButton.Rect.Height + 20) * i); + var activeObjective = segments[activeObjectives[i]]; + activeObjective.ReplayButton.RectTransform.AbsoluteOffset = new Point(0, (activeObjective.ReplayButton.Rect.Height + 20) * i); } } @@ -492,32 +430,25 @@ namespace Barotrauma.Tutorials return true; } - protected GUIComponent CreateInfoFrame(string title, string text, int width = 300, int height = 80, string anchorStr = "", bool hasButton = false, Action callback = null, Action showVideo = null) + protected GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action callback = null, Action showVideo = null) { if (hasButton) height += 60; - Anchor anchor = Anchor.TopRight; - - if (anchorStr != string.Empty) - { - Enum.TryParse(anchorStr, out anchor); - } - width = (int)(width * GUI.Scale); height = (int)(height * GUI.Scale); - string wrappedText = ToolBox.WrapText(text, width, GUI.Font); - height += (int)GUI.Font.MeasureString(wrappedText).Y; + LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); + height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; if (title.Length > 0) { - height += (int)GUI.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); + height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); } var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); - infoBlock.Flash(GUI.Style.Green); + infoBlock.Flash(GUIStyle.Green); var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) { @@ -528,20 +459,12 @@ namespace Barotrauma.Tutorials if (title.Length > 0) { var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), - title, font: GUI.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); + title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); titleBlock.RectTransform.IsFixedSize = true; } - List richTextData = RichTextData.GetRichTextData(" " + text, out text); - GUITextBlock textBlock; - if (richTextData == null) - { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); - } - else - { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), richTextData, text, wrap: true); - } + text = RichString.Rich(text); + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); textBlock.RectTransform.IsFixedSize = true; infoBoxClosedCallback = callback; @@ -589,22 +512,26 @@ namespace Barotrauma.Tutorials #endregion #region Video - protected void LoadVideo(TutorialSegment segment) + protected void LoadVideo(Segment segment) { if (videoPlayer == null) videoPlayer = new VideoPlayer(); - if (segment.ContentType != TutorialContentTypes.ManualVideo) + if (segment.ContentType != TutorialContentType.ManualVideo) { - videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, segment.Objective, StopCurrentContentSegment); + videoPlayer.LoadContent( + PlayableContentPath, + new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), + new VideoPlayer.TextSettings(segment.VideoContent.Value.TextTag, segment.VideoContent.Value.Width), + segment.Id, true, segment.Objective, StopCurrentContentSegment); } else { - videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), null, segment.Id, true, string.Empty, null); + videoPlayer.LoadContent(PlayableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), null, segment.Id, true, string.Empty, null); } } #endregion #region Highlights - protected void HighlightInventorySlot(Inventory inventory, string identifier, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) + protected void HighlightInventorySlot(Inventory inventory, Identifier identifier, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) { if (inventory.visualSlots == null) { return; } for (int i = 0; i < inventory.Capacity; i++) @@ -616,7 +543,7 @@ namespace Barotrauma.Tutorials } } - protected void HighlightInventorySlotWithTag(Inventory inventory, string tag, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) + protected void HighlightInventorySlotWithTag(Inventory inventory, Identifier tag, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) { if (inventory.visualSlots == null) { return; } for (int i = 0; i < inventory.Capacity; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index d9ec85f14..2b904f82f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -5,11 +5,6 @@ namespace Barotrauma class TutorialMode : GameMode { public Tutorial Tutorial; - - public static void StartTutorial(Tutorial tutorial) - { - tutorial.Initialize(); - } public TutorialMode(GameModePreset preset) : base(preset) @@ -20,7 +15,6 @@ namespace Barotrauma { base.Start(); GameMain.GameSession.CrewManager = new CrewManager(true); - Tutorial.Start(); foreach (Item item in Item.ItemList) { //don't consider the items to belong in the outpost to prevent the stealing icon from showing diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 113f7e5c3..14daa8a4a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -64,12 +64,12 @@ namespace Barotrauma GameMain.Instance.ResolutionChanged -= CreateTopLeftButtons; }; int buttonHeight = GUI.IntScale(40); - Vector2 buttonSpriteSize = GUI.Style.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; + Vector2 buttonSpriteSize = GUIStyle.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; int buttonWidth = (int)((buttonHeight / buttonSpriteSize.Y) * buttonSpriteSize.X); Point buttonSize = new Point(buttonWidth, buttonHeight); crewListButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "CrewListToggleButton") { - ToolTip = TextManager.GetWithVariable("hudbutton.crewlist", "[key]", GameMain.Config.KeyBindText(InputType.CrewOrders)), + ToolTip = TextManager.GetWithVariable("hudbutton.crewlist", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.CrewOrders)), OnClicked = (GUIButton btn, object userdata) => { if (CrewManager == null) { return false; } @@ -79,7 +79,7 @@ namespace Barotrauma }; commandButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "CommandButton") { - ToolTip = TextManager.GetWithVariable("hudbutton.commandinterface", "[key]", GameMain.Config.KeyBindText(InputType.Command)), + ToolTip = TextManager.GetWithVariable("hudbutton.commandinterface", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)), OnClicked = (button, userData) => { if (CrewManager == null) { return false; } @@ -89,7 +89,7 @@ namespace Barotrauma }; tabMenuButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "TabMenuButton") { - ToolTip = TextManager.GetWithVariable("hudbutton.tabmenu", "[key]", GameMain.Config.KeyBindText(InputType.InfoTab)), + ToolTip = TextManager.GetWithVariable("hudbutton.tabmenu", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.InfoTab)), OnClicked = (button, userData) => ToggleTabMenu() }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 40cff56c3..a7ee9cc4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -13,14 +13,14 @@ namespace Barotrauma { private const string HintManagerFile = "hintmanager.xml"; - public static bool Enabled => GameMain.Config != null && !GameMain.Config.DisableInGameHints; - private static HashSet HintIdentifiers { get; set; } - private static Dictionary> HintTags { get; } = new Dictionary>(); - private static Dictionary HintOrders { get; } = new Dictionary(); + public static bool Enabled => !GameSettings.CurrentConfig.DisableInGameHints; + private static HashSet HintIdentifiers { get; set; } + private static Dictionary> HintTags { get; } = new Dictionary>(); + private static Dictionary HintOrders { get; } = new Dictionary(); /// /// Hints that have already been shown this round and shouldn't be shown shown again until the next round /// - private static HashSet HintsIgnoredThisRound { get; } = new HashSet(); + private static HashSet HintsIgnoredThisRound { get; } = new HashSet(); private static GUIMessageBox ActiveHintMessageBox { get; set; } private static Action OnUpdate { get; set; } private static double TimeStoppedInteracting { get; set; } @@ -43,10 +43,10 @@ namespace Barotrauma var doc = XMLExtensions.TryLoadXml(HintManagerFile); if (doc?.Root != null) { - HintIdentifiers = new HashSet(); + HintIdentifiers = new HashSet(); foreach (var element in doc.Root.Elements()) { - GetHintsRecursive(element, element.Name.ToString()); + GetHintsRecursive(element, element.NameAsIdentifier()); } } else @@ -59,18 +59,18 @@ namespace Barotrauma DebugConsole.ThrowError($"File \"{HintManagerFile}\" is missing - cannot initialize the HintManager!"); } - static void GetHintsRecursive(XElement element, string identifier) + static void GetHintsRecursive(XElement element, Identifier identifier) { if (!element.HasElements) { HintIdentifiers.Add(identifier); - if (element.GetAttributeStringArray("tags", null, convertToLowerInvariant: true) is string[] tags) + if (element.GetAttributeIdentifierArray("tags", null) is Identifier[] tags) { HintTags.TryAdd(identifier, tags.ToHashSet()); } - if (element.GetAttributeString("order", null) is string orderIdentifier && !string.IsNullOrEmpty(orderIdentifier)) + if (element.GetAttributeIdentifier("order", Identifier.Empty) is Identifier orderIdentifier && orderIdentifier != Identifier.Empty) { - string orderOption = element.GetAttributeString("orderoption", ""); + Identifier orderOption = element.GetAttributeIdentifier("orderoption", Identifier.Empty); HintOrders.Add(identifier, (orderIdentifier, orderOption)); } return; @@ -82,14 +82,14 @@ namespace Barotrauma } foreach (var childElement in element.Elements()) { - GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}"); + GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}".ToIdentifier()); } } } public static void Update() { - if (HintIdentifiers == null || GameMain.Config.DisableInGameHints) { return; } + if (HintIdentifiers == null || GameSettings.CurrentConfig.DisableInGameHints) { return; } if (GameMain.GameSession == null || !GameMain.GameSession.IsRunning) { return; } if (ActiveHintMessageBox != null) @@ -137,7 +137,7 @@ namespace Barotrauma // onstartedinteracting.brokenitem if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { - if (DisplayHint($"{hintIdentifierBase}.brokenitem")) { return; } + if (DisplayHint($"{hintIdentifierBase}.brokenitem".ToIdentifier())) { return; } } // Don't display other item-related hints if the repair interface is displayed @@ -147,22 +147,25 @@ namespace Barotrauma if (item.Submarine?.Info?.Type == SubmarineType.Outpost && item.ContainedItems.Any(i => !i.AllowStealing)) { - if (DisplayHint($"{hintIdentifierBase}.lootingisstealing")) { return; } + if (DisplayHint($"{hintIdentifierBase}.lootingisstealing".ToIdentifier())) { return; } } // onstartedinteracting.turretperiscope if (item.HasTag("periscope") && item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag("turret")) is Turret) { - if (DisplayHint($"{hintIdentifierBase}.turretperiscope", - variableTags: new string[] { "[shootkey]", "[deselectkey]", }, - variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect) })) + if (DisplayHint($"{hintIdentifierBase}.turretperiscope".ToIdentifier(), + variables: new[] + { + ("[shootkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)), + ("[deselectkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)) + })) { return; } } // onstartedinteracting.item... hintIdentifierBase += ".item"; - foreach (string hintIdentifier in HintIdentifiers) + foreach (Identifier hintIdentifier in HintIdentifiers) { if (!hintIdentifier.StartsWith(hintIdentifierBase)) { continue; } if (!HintTags.TryGetValue(hintIdentifier, out var hintTags)) { continue; } @@ -180,7 +183,7 @@ namespace Barotrauma Character.Controlled.SelectedConstruction.OwnInventory?.AllItems is IEnumerable containedItems && containedItems.Count(i => i.HasTag("reactorfuel")) > 1) { - if (DisplayHint("onisinteracting.reactorwithextrarods")) { return; } + if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; } } } @@ -233,11 +236,11 @@ namespace Barotrauma } if (!GameMain.GameSession.GameMode.IsSinglePlayer && - GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) + GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { - DisplayHint("onroundstarted.voipdisabled", onUpdate: () => + DisplayHint("onroundstarted.voipdisabled".ToIdentifier(), onUpdate: () => { - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) { return; } + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { return; } ActiveHintMessageBox.Close(); }); } @@ -271,7 +274,7 @@ namespace Barotrauma if (spottedCharacter == null || spottedCharacter.Removed || spottedCharacter.IsDead) { return; } if (Character.Controlled.SelectedConstruction != sonar) { return; } if (HumanAIController.IsFriendly(Character.Controlled, spottedCharacter)) { return; } - DisplayHint("onsonarspottedenemy"); + DisplayHint("onsonarspottedenemy".ToIdentifier()); } public static void OnAfflictionDisplayed(Character character, List displayedAfflictions) @@ -285,9 +288,8 @@ namespace Barotrauma if (affliction.Prefab == AfflictionPrefab.OxygenLow) { continue; } if (affliction.Prefab == AfflictionPrefab.RadiationSickness && (GameMain.GameSession.Map?.Radiation?.IsEntityRadiated(character) ?? false)) { continue; } if (affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; } - DisplayHint("onafflictiondisplayed", - variableTags: new string[1] { "[key]" }, - variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Health) }, + DisplayHint("onafflictiondisplayed".ToIdentifier(), + variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)) }, icon: affliction.Prefab.Icon, iconColor: CharacterHealth.GetAfflictionIconColor(affliction), onUpdate: () => @@ -308,12 +310,11 @@ namespace Barotrauma if (TimeStoppedInteracting + 1 > Timing.TotalTime) { return; } if (GUI.MouseOn != null) { return; } if (Character.Controlled.Inventory?.visualSlots != null && Character.Controlled.Inventory.visualSlots.Any(s => s.InteractRect.Contains(PlayerInput.MousePosition))) { return; } - string hintIdentifier = "onshootwithoutaiming"; + Identifier hintIdentifier = "onshootwithoutaiming".ToIdentifier(); if (!HintTags.TryGetValue(hintIdentifier, out var tags)) { return; } if (!item.HasTag(tags)) { return; } DisplayHint(hintIdentifier, - variableTags: new string[1] { "[key]" }, - variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Aim) }, + variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim)) }, onUpdate: () => { if (character.SelectedConstruction == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim)) @@ -328,21 +329,21 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (door == null || door.Stuck < 20.0f) { return; } - DisplayHint("onweldingdoor"); + DisplayHint("onweldingdoor".ToIdentifier()); } public static void OnTryOpenStuckDoor(Character character) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } - DisplayHint("ontryopenstuckdoor"); + DisplayHint("ontryopenstuckdoor".ToIdentifier()); } public static void OnShowCampaignInterface(CampaignMode.InteractionType interactionType) { if (!CanDisplayHints()) { return; } if (interactionType == CampaignMode.InteractionType.None) { return; } - string hintIdentifier = $"onshowcampaigninterface.{interactionType.ToString().ToLowerInvariant()}"; + Identifier hintIdentifier = $"onshowcampaigninterface.{interactionType}".ToIdentifier(); DisplayHint(hintIdentifier, onUpdate: () => { @@ -359,7 +360,7 @@ namespace Barotrauma { IgnoreReminder("commandinterface"); if (!CanDisplayHints()) { return; } - DisplayHint("onshowcommandinterface", onUpdate: () => + DisplayHint("onshowcommandinterface".ToIdentifier(), onUpdate: () => { if (CrewManager.IsCommandInterfaceOpen) { return; } ActiveHintMessageBox.Close(); @@ -370,7 +371,7 @@ namespace Barotrauma { if (!CanDisplayHints()) { return; } if (CharacterHealth.OpenHealthWindow == null) { return; } - DisplayHint("onshowhealthinterface", onUpdate: () => + DisplayHint("onshowhealthinterface".ToIdentifier(), onUpdate: () => { if (CharacterHealth.OpenHealthWindow != null) { return; } ActiveHintMessageBox.Close(); @@ -387,7 +388,7 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (item == null || item.AllowStealing || !item.StolenDuringRound) { return; } - DisplayHint("onstoleitem", onUpdate: () => + DisplayHint("onstoleitem".ToIdentifier(), onUpdate: () => { if (item == null || item.Removed || item.GetRootInventoryOwner() != character) { @@ -400,7 +401,7 @@ namespace Barotrauma { if (!CanDisplayHints()) { return; } if (character != Character.Controlled || !character.LockHands) { return; } - DisplayHint("onhandcuffed", onUpdate: () => + DisplayHint("onhandcuffed".ToIdentifier(), onUpdate: () => { if (character != null && !character.Removed && character.LockHands) { return; } ActiveHintMessageBox.Close(); @@ -413,7 +414,7 @@ namespace Barotrauma if (reactor == null) { return; } if (reactor.Item.Submarine?.Info?.Type != SubmarineType.Player || reactor.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } if (!HasValidJob("engineer")) { return; } - DisplayHint("onreactoroutoffuel", onUpdate: () => + DisplayHint("onreactoroutoffuel".ToIdentifier(), onUpdate: () => { if (reactor?.Item != null && !reactor.Item.Removed && reactor.AvailableFuel < 1) { return; } ActiveHintMessageBox.Close(); @@ -424,13 +425,13 @@ namespace Barotrauma { if (!CanDisplayHints()) { return; } if (transitionType == CampaignMode.TransitionType.None) { return; } - DisplayHint($"onavailabletransition.{transitionType.ToString().ToLowerInvariant()}"); + DisplayHint($"onavailabletransition.{transitionType}".ToIdentifier()); } public static void OnShowSubInventory(Item item) { if (item?.Prefab == null) { return; } - if (item.Prefab.Identifier.Equals("toolbelt", StringComparison.OrdinalIgnoreCase)) + if (item.Prefab.Identifier == "toolbelt") { IgnoreReminder("toolbelt"); } @@ -447,7 +448,7 @@ namespace Barotrauma if (character != Character.Controlled) { return; } if (character.IsDead) { return; } if (character.CharacterHealth != null && character.Vitality < character.CharacterHealth.MinVitality) { return; } - DisplayHint("oncharacterunconscious"); + DisplayHint("oncharacterunconscious".ToIdentifier()); } public static void OnCharacterKilled(Character character) @@ -457,21 +458,21 @@ namespace Barotrauma if (GameMain.IsMultiplayer) { return; } if (GameMain.GameSession?.CrewManager == null) { return; } if (GameMain.GameSession.CrewManager.GetCharacters().None(c => !c.IsDead)) { return; } - DisplayHint("oncharacterkilled"); + DisplayHint("oncharacterkilled".ToIdentifier()); } private static void OnStartedControlling() { if (Level.IsLoadedOutpost) { return; } if (Character.Controlled?.Info?.Job?.Prefab == null) { return; } - string hintIdentifier = $"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}"; + Identifier hintIdentifier = $"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}".ToIdentifier(); DisplayHint(hintIdentifier, icon: Character.Controlled.Info.Job.Prefab.Icon, iconColor: Character.Controlled.Info.Job.Prefab.UIColor, onDisplay: () => { if (!HintOrders.TryGetValue(hintIdentifier, out var orderInfo)) { return; } - var orderPrefab = Order.GetPrefab(orderInfo.identifier); + var orderPrefab = OrderPrefab.Prefabs[orderInfo.identifier]; if (orderPrefab == null) { return; } Item targetEntity = null; ItemComponent targetItem = null; @@ -481,8 +482,8 @@ namespace Barotrauma if (targetEntity == null) { return; } targetItem = orderPrefab.GetTargetItemComponent(targetEntity); } - var order = new Order(orderPrefab, targetEntity as Entity, targetItem, orderGiver: Character.Controlled); - GameMain.GameSession?.CrewManager?.SetCharacterOrder(Character.Controlled, order, orderInfo.option, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + var order = new Order(orderPrefab, orderInfo.option, targetEntity, targetItem, orderGiver: Character.Controlled).WithManualPriority(CharacterInfo.HighestManualOrderPriority); + GameMain.GameSession.CrewManager.SetCharacterOrder(Character.Controlled, order); }); } @@ -498,7 +499,7 @@ namespace Barotrauma if (!steering.SteeringPath.Finished && steering.SteeringPath.NextNode != null) { return; } if (steering.LevelStartSelected && (Level.Loaded.StartOutpost == null || !steering.Item.Submarine.AtStartExit)) { return; } if (steering.LevelEndSelected && (Level.Loaded.EndOutpost == null || !steering.Item.Submarine.AtEndExit)) { return; } - DisplayHint("onautopilotreachedoutpost"); + DisplayHint("onautopilotreachedoutpost".ToIdentifier()); } public static void OnStatusEffectApplied(ItemComponent component, ActionType actionType, Character character) @@ -507,7 +508,7 @@ namespace Barotrauma if (character != Character.Controlled) { return; } // Could make this more generic if there will ever be any other status effect related hints if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; } - DisplayHint("onrepairfailed"); + DisplayHint("onrepairfailed".ToIdentifier()); } public static void OnActiveOrderAdded(Order order) @@ -519,7 +520,7 @@ namespace Barotrauma order.TargetEntity is Hull h && h.Submarine?.TeamID == Character.Controlled.TeamID) { - DisplayHint("onballastflorainfected"); + DisplayHint("onballastflorainfected".ToIdentifier()); } } @@ -529,7 +530,7 @@ namespace Barotrauma var divingGear = Character.Controlled.GetEquippedItem("diving", InvSlotType.OuterClothes); if (divingGear?.OwnInventory == null) { return; } if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; } - DisplayHint("ondivinggearoutofoxygen", onUpdate: () => + DisplayHint("ondivinggearoutofoxygen".ToIdentifier(), onUpdate: () => { if (divingGear == null || divingGear.Removed || Character.Controlled == null || !Character.Controlled.HasEquippedItem(divingGear) || @@ -546,7 +547,7 @@ namespace Barotrauma if (Character.Controlled.CurrentHull == null) { return; } if (HumanAIController.IsBallastFloraNoticeable(Character.Controlled, Character.Controlled.CurrentHull)) { - if (IsOnFriendlySub() && DisplayHint("onballastflorainfected")) { return; } + if (IsOnFriendlySub() && DisplayHint("onballastflorainfected".ToIdentifier())) { return; } } foreach (var gap in Character.Controlled.CurrentHull.ConnectedGaps) { @@ -556,7 +557,7 @@ namespace Barotrauma { if (!IsWearingDivingSuit()) { continue; } if (Character.Controlled.IsProtectedFromPressure()) { continue; } - if (DisplayHint("divingsuitwarning", extendTextTag: false)) { return; } + if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; } continue; } foreach (var me in gap.linkedTo) @@ -565,8 +566,8 @@ namespace Barotrauma if (!(me is Hull adjacentHull)) { continue; } if (!IsOnFriendlySub()) { continue; } if (IsWearingDivingSuit()) { continue; } - if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure")) { return; } - if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage")) { return; } + if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; } + if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage".ToIdentifier())) { return; } } static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem("deepdiving", InvSlotType.OuterClothes) is Item; @@ -586,7 +587,7 @@ namespace Barotrauma if (GameMain.GameSession.GameMode.IsSinglePlayer) { - if (DisplayHint($"{hintIdentifierBase}.characterchange")) + if (DisplayHint($"{hintIdentifierBase}.characterchange".ToIdentifier())) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; @@ -595,9 +596,8 @@ namespace Barotrauma if (Level.Loaded.Type != LevelData.LevelType.Outpost) { - if (DisplayHint($"{hintIdentifierBase}.commandinterface", - variableTags: new string[] { "[commandkey]" }, - variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Command) }, + if (DisplayHint($"{hintIdentifierBase}.commandinterface".ToIdentifier(), + variables: new[] { ("[commandkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)) }, onUpdate: () => { if (!CrewManager.IsCommandInterfaceOpen) { return; } @@ -609,9 +609,8 @@ namespace Barotrauma } } - if (DisplayHint($"{hintIdentifierBase}.tabmenu", - variableTags: new string[] { "[infotabkey]" }, - variableValues: new string[] { GameMain.Config.KeyBindText(InputType.InfoTab) }, + if (DisplayHint($"{hintIdentifierBase}.tabmenu".ToIdentifier(), + variables: new[] { ("[infotabkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.InfoTab)) }, onUpdate: () => { if (!GameSession.IsTabMenuOpen) { return; } @@ -624,7 +623,7 @@ namespace Barotrauma if (Character.Controlled.Inventory?.GetItemInLimbSlot(InvSlotType.Bag)?.Prefab?.Identifier == "toolbelt") { - if (DisplayHint($"{hintIdentifierBase}.toolbelt")) + if (DisplayHint($"{hintIdentifierBase}.toolbelt".ToIdentifier())) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; @@ -632,25 +631,25 @@ namespace Barotrauma } } - private static bool DisplayHint(string hintIdentifier, bool extendTextTag = true, string[] variableTags = null, string[] variableValues = null, Sprite icon = null, Color? iconColor = null, Action onDisplay = null, Action onUpdate = null) + private static bool DisplayHint(Identifier hintIdentifier, bool extendTextTag = true, (Identifier Tag, LocalizedString Value)[] variables = null, Sprite icon = null, Color? iconColor = null, Action onDisplay = null, Action onUpdate = null) { - if (string.IsNullOrEmpty(hintIdentifier)) { return false; } + if (hintIdentifier == Identifier.Empty) { return false; } if (!HintIdentifiers.Contains(hintIdentifier)) { return false; } - if (GameMain.Config.IgnoredHints.Contains(hintIdentifier)) { return false; } + if (IgnoredHints.Instance.Contains(hintIdentifier)) { return false; } if (HintsIgnoredThisRound.Contains(hintIdentifier)) { return false; } - string text; - string textTag = extendTextTag ? $"hint.{hintIdentifier}" : hintIdentifier; - if (variableTags != null && variableTags != null && variableTags.Length > 0 && variableTags.Length == variableValues.Length) + LocalizedString text; + Identifier textTag = extendTextTag ? $"hint.{hintIdentifier}".ToIdentifier() : hintIdentifier; + if (variables != null && variables.Length > 0) { - text = TextManager.GetWithVariables(textTag, variableTags, variableValues, returnNull: true); + text = TextManager.GetWithVariables(textTag, variables); } else { - text = TextManager.Get(textTag, returnNull: true); + text = TextManager.Get(textTag); } - if (string.IsNullOrEmpty(text)) + if (text.IsNullOrEmpty()) { #if DEBUG DebugConsole.ThrowError($"No hint text found for text tag \"{textTag}\""); @@ -668,20 +667,20 @@ namespace Barotrauma ActiveHintMessageBox.InnerFrame.Flash(color: iconColor ?? Color.Orange, flashDuration: 0.75f); onDisplay?.Invoke(); - GameAnalyticsManager.AddDesignEvent($"HintManager:{GameMain.GameSession?.GameMode?.Preset?.Identifier ?? "none"}:HintDisplayed:{hintIdentifier}"); + GameAnalyticsManager.AddDesignEvent($"HintManager:{GameMain.GameSession?.GameMode?.Preset?.Identifier ?? "none".ToIdentifier()}:HintDisplayed:{hintIdentifier}"); return true; } public static bool OnDontShowAgain(GUITickBox tickBox) { - IgnoreHint((string)tickBox.UserData, ignore: tickBox.Selected); + IgnoreHint((Identifier)tickBox.UserData, ignore: tickBox.Selected); return true; } - private static void IgnoreHint(string hintIdentifier, bool ignore = true) + private static void IgnoreHint(Identifier hintIdentifier, bool ignore = true) { - if (string.IsNullOrEmpty(hintIdentifier)) { return; } + if (hintIdentifier.IsEmpty) { return; } if (!HintIdentifiers.Contains(hintIdentifier)) { #if DEBUG @@ -691,29 +690,32 @@ namespace Barotrauma } if (ignore) { - GameMain.Config.IgnoredHints.Add(hintIdentifier); + IgnoredHints.Instance.Add(hintIdentifier); } else { - GameMain.Config.IgnoredHints.Remove(hintIdentifier); + IgnoredHints.Instance.Remove(hintIdentifier); } } private static void IgnoreReminder(string reminderIdentifier) { - HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}"); + HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}".ToIdentifier()); } public static bool OnDisableHints(GUITickBox tickBox) { - GameMain.Config.DisableInGameHints = tickBox.Selected; - return GameMain.Config.SaveNewPlayerConfig(); + var config = GameSettings.CurrentConfig; + config.DisableInGameHints = tickBox.Selected; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); + return true; } private static bool CanDisplayHints(bool requireGameScreen = true, bool requireControllingCharacter = true) { if (HintIdentifiers == null) { return false; } - if (GameMain.Config.DisableInGameHints) { return false; } + if (GameSettings.CurrentConfig.DisableInGameHints) { return false; } if (ActiveHintMessageBox != null) { return false; } if (requireControllingCharacter && Character.Controlled == null) { return false; } var gameMode = GameMain.GameSession?.GameMode; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index e6d7114bc..003816dae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -9,16 +9,18 @@ namespace Barotrauma { internal partial class ReadyCheck { - private static string readyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); + private static LocalizedString readyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); - private static string readyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", new[] { "[ready]", "[total]" }, new[] { ready.ToString(), total.ToString() }); - private static string readyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); + private static LocalizedString readyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", + ("[ready]", ready.ToString()), + ("[total]", total.ToString())); + private static LocalizedString readyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); - private static readonly string readyCheckHeader = TextManager.Get("ReadyCheck.Title"); + private static readonly LocalizedString readyCheckHeader = TextManager.Get("ReadyCheck.Title"); - private static readonly string noButton = TextManager.Get("No"), - yesButton = TextManager.Get("Yes"), - closeButton = TextManager.Get("Close"); + private static readonly LocalizedString noButton = TextManager.Get("No"), + yesButton = TextManager.Get("Yes"), + closeButton = TextManager.Get("Close"); private const string TimerData = "Timer", PromptData = "ReadyCheck", @@ -42,7 +44,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center); - new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), time / endTime, GUI.Style.Orange) { UserData = TimerData }; + new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), time / endTime, GUIStyle.Orange) { UserData = TimerData }; // Yes msgBox.Buttons[0].OnClicked = delegate @@ -222,7 +224,7 @@ namespace Barotrauma int readyCount = Clients.Count(pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; - GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount), ChatMessageType.Server, null)); + GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); } private void UpdateState(byte id, ReadyStatus status) @@ -256,7 +258,7 @@ namespace Barotrauma return; } - image.ApplyStyle(GUI.Style.GetComponentStyle(style)); + image.ApplyStyle(GUIStyle.GetComponentStyle(style)); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index f0db4ea5b..1a0f12157 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -59,7 +59,7 @@ namespace Barotrauma if (!singleplayer) { - SoundPlayer.OverrideMusicType = gameOver ? "crewdead" : "endround"; + SoundPlayer.OverrideMusicType = (gameOver ? "crewdead" : "endround").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; } @@ -84,7 +84,7 @@ namespace Barotrauma }; var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent.RectTransform), - TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader.Rect.Height * 2.0f)); CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2)); @@ -101,19 +101,19 @@ namespace Barotrauma Stretch = true }; var crewHeader2 = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent2.RectTransform), - CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2)); } //header ------------------------------------------------------------------------------- - string headerText = GetHeaderText(gameOver, transitionType); + LocalizedString headerText = GetHeaderText(gameOver, transitionType); GUITextBlock headerTextBlock = null; - if (!string.IsNullOrEmpty(headerText)) + if (!headerText.IsNullOrEmpty()) { headerTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), crewFrame.RectTransform, Anchor.TopLeft, Pivot.BottomLeft), - headerText, textAlignment: Alignment.BottomLeft, font: GUI.LargeFont, wrap: true); + headerText, textAlignment: Alignment.BottomLeft, font: GUIStyle.LargeFont, wrap: true); } //traitor panel ------------------------------------------------------------------------------- @@ -130,14 +130,14 @@ namespace Barotrauma }; var traitorHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorContent.RectTransform), - TextManager.Get("traitors"), font: GUI.SubHeadingFont); + TextManager.Get("traitors"), font: GUIStyle.SubHeadingFont); traitorHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(traitorHeader.Rect.Height * 2.0f)); GUIListBox listBox = CreateCrewList(traitorContent, traitorResults.SelectMany(tr => tr.Characters.Select(c => c.Info))); foreach (var traitorResult in traitorResults) { - var traitorMission = TraitorMissionPrefab.List.Find(t => t.Identifier == traitorResult.MissionIdentifier); + var traitorMission = TraitorMissionPrefab.Prefabs.Find(t => t.Identifier == traitorResult.MissionIdentifier); if (traitorMission == null) { continue; } //spacing @@ -154,8 +154,8 @@ namespace Barotrauma Color = traitorMission.IconColor }; - string traitorMessage = TextManager.GetServerMessage(traitorResult.EndMessage); - if (!string.IsNullOrEmpty(traitorMessage)) + LocalizedString traitorMessage = TextManager.GetServerMessage(traitorResult.EndMessage); + if (!traitorMessage.IsNullOrEmpty()) { var textContent = new GUILayoutGroup(new RectTransform(Vector2.One, traitorResultHorizontal.RectTransform)) { @@ -164,10 +164,10 @@ namespace Barotrauma var traitorStatusText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get(traitorResult.Success ? "missioncompleted" : "missionfailed"), - textColor: traitorResult.Success ? GUI.Style.Green : GUI.Style.Red, font: GUI.SubHeadingFont); + textColor: traitorResult.Success ? GUIStyle.Green : GUIStyle.Red, font: GUIStyle.SubHeadingFont); var traitorMissionInfo = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - traitorMessage, font: GUI.SmallFont, wrap: true); + traitorMessage, font: GUIStyle.SmallFont, wrap: true); traitorResultHorizontal.Recalculate(); @@ -196,7 +196,7 @@ namespace Barotrauma }; var reputationHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), reputationContent.RectTransform), - TextManager.Get("reputation"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + TextManager.Get("reputation"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); reputationHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(reputationHeader.Rect.Height * 2.0f)); CreateReputationInfoPanel(reputationContent, campaignMode); @@ -233,7 +233,7 @@ namespace Barotrauma if (missionsToDisplay.Any()) { var missionHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), - TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); missionHeader.RectTransform.MinSize = new Point(0, (int)(missionHeader.Rect.Height * 1.2f)); } @@ -271,7 +271,7 @@ namespace Barotrauma Stretch = true }; - string missionMessage = + LocalizedString missionMessage = selectedMissions.Contains(displayedMission) ? displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : displayedMission.Description; @@ -293,7 +293,7 @@ namespace Barotrauma }; missionContentHorizontal.Recalculate(); var missionNameTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - displayedMission.Name, font: GUI.SubHeadingFont); + displayedMission.Name, font: GUIStyle.SubHeadingFont); if (displayedMission.Difficulty.HasValue) { var groupSize = missionNameTextBlock.Rect.Size; @@ -314,12 +314,12 @@ namespace Barotrauma } } var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - missionMessage, wrap: true, parseRichText: true); + RichString.Rich(missionMessage), wrap: true); int reward = displayedMission.GetReward(Submarine.MainSub); if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), displayedMission.GetMissionRewardText(Submarine.MainSub), parseRichText: true); + LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); } if (displayedMission != missionsToDisplay.Last()) @@ -346,7 +346,7 @@ namespace Barotrauma GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContentHorizontal.RectTransform), - TextManager.Get("nomission"), font: GUI.LargeFont); + TextManager.Get("nomission"), font: GUIStyle.LargeFont); } /*missionContentHorizontal.Recalculate(); @@ -397,7 +397,7 @@ namespace Barotrauma if (startLocation.Type.HasOutpost && startLocation.Reputation != null) { - var iconStyle = GUI.Style.GetComponentStyle("LocationReputationIcon"); + var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon"); var locationFrame = CreateReputationElement( reputationList.Content, startLocation.Name, @@ -452,8 +452,8 @@ namespace Barotrauma var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; var unlockEvent = - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && string.IsNullOrEmpty(ep.BiomeIdentifier)); + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); if (unlockEvent == null) { continue; } if (string.IsNullOrEmpty(unlockEvent.UnlockPathFaction) || unlockEvent.UnlockPathFaction.Equals("location", StringComparison.OrdinalIgnoreCase)) @@ -462,7 +462,7 @@ namespace Barotrauma } else { - if (faction == null || !faction.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)) { continue; } + if (faction == null || faction.Prefab.Identifier != unlockEvent.UnlockPathFaction) { continue; } } if (unlockEvent != null) @@ -471,20 +471,20 @@ namespace Barotrauma Faction unlockFaction = null; if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)); + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); unlockReputation = unlockFaction?.Reputation; } float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); - string unlockText = TextManager.GetWithVariables( + RichString unlockText = RichString.Rich(TextManager.GetWithVariables( "lockedpathreputationrequirement", - new string[] { "[reputation]", "[biomename]" }, - new string[] { Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true), $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖" }); + ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, - unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUI.Style.TextColor, parseRichText: true); + unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) { - unlockInfoPanel.Font = GUI.SmallFont; + unlockInfoPanel.Font = GUIStyle.SmallFont; } } } @@ -492,7 +492,7 @@ namespace Barotrauma } } - private string GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) + private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name; @@ -539,14 +539,14 @@ namespace Barotrauma locationName = "[UNKNOWN]"; } - string subName = string.Empty; + LocalizedString subName = string.Empty; SubmarineInfo currentOrPending = SubmarineSelection.CurrentOrPendingSubmarine(); if (currentOrPending != null) { subName = currentOrPending.DisplayName; } - return TextManager.GetWithVariables(textTag, new string[2] { "[sub]", "[location]" }, new string[2] { subName, locationName }); + return TextManager.GetWithVariables(textTag, ("[sub]", subName), ("[location]", locationName)); } private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos) @@ -566,9 +566,9 @@ namespace Barotrauma characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); statusButton.RectTransform.RelativeSize = new Vector2(statusColumnWidthPercentage * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = statusButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = statusButton.TextBlock.Font = GUIStyle.HotkeyFont; jobButton.CanBeFocused = characterButton.CanBeFocused = statusButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = statusButton.ForceUpperCase = true; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = statusButton.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -615,10 +615,10 @@ namespace Barotrauma }; GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(characterInfo.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: characterInfo.Job.Prefab.UIColor); + ToolBox.LimitString(characterInfo.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: characterInfo.Job.Prefab.UIColor); - string statusText = TextManager.Get("StatusOK"); - Color statusColor = GUI.Style.Green; + LocalizedString statusText = TextManager.Get("StatusOK"); + Color statusColor = GUIStyle.Green; Character character = characterInfo.Character; if (character == null || character.IsDead) @@ -626,7 +626,7 @@ namespace Barotrauma if (character == null && characterInfo.IsNewHire && characterInfo.CauseOfDeath == null) { statusText = TextManager.Get("CampaignCrew.NewHire"); - statusColor = GUI.Style.Blue; + statusColor = GUIStyle.Blue; } else if (characterInfo.CauseOfDeath == null) { @@ -637,9 +637,9 @@ namespace Barotrauma { string errorMsg = "Character \"[name]\" had an invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified)."; DebugConsole.ThrowError(errorMsg.Replace("[name]", characterInfo.Name)); - GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", characterInfo.SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", characterInfo.SpeciesName.Value)); statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); - statusColor = GUI.Style.Red; + statusColor = GUIStyle.Red; } else { @@ -664,12 +664,12 @@ namespace Barotrauma } GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(statusText, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); + ToolBox.LimitString(statusText.Value, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); } private GUIFrame CreateReputationElement(GUIComponent parent, - string name, float reputation, float normalizedReputation, float initialReputation, - string shortDescription, string fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) + LocalizedString name, float reputation, float normalizedReputation, float initialReputation, + LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); @@ -704,7 +704,7 @@ namespace Barotrauma factionInfoHorizontal.Recalculate(); var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), - name, font: GUI.SubHeadingFont) + name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, UserData = "header" @@ -723,34 +723,34 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); - string reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); + LocalizedString reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); int reputationChange = (int)Math.Round(reputation - initialReputation); if (Math.Abs(reputationChange) > 0) { string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUI.Style.Green : GUI.Style.Red); - var rtData = RichTextData.GetRichTextData($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)", out string sanitizedText); + string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - rtData, sanitizedText, - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); + richText, + textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); } else { new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - reputationText, - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont, parseRichText: true); + RichString.Rich(reputationText), + textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); } //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), factionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(5)) }, style: null); var factionDescription = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.6f), factionTextContent.RectTransform), - shortDescription, font: GUI.SmallFont, wrap: true) + shortDescription, font: GUIStyle.SmallFont, wrap: true) { UserData = "description", Padding = Vector4.Zero }; - if (shortDescription != fullDescription && !string.IsNullOrEmpty(fullDescription)) + if (shortDescription != fullDescription && !fullDescription.IsNullOrEmpty()) { factionDescription.ToolTip = fullDescription; } @@ -771,16 +771,16 @@ namespace Barotrauma for (int i = 0; i < 5; i++) { GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), Reputation.GetReputationColor(i / 5.0f), isFilled: true); - GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), GUI.Style.ColorInventoryBackground, isFilled: false); + GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), GUIStyle.ColorInventoryBackground, isFilled: false); } - GUI.DrawRectangle(sb, rect, GUI.Style.ColorInventoryBackground, isFilled: false); + GUI.DrawRectangle(sb, rect, GUIStyle.ColorInventoryBackground, isFilled: false); - GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUI.Style.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); - GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUI.Style.TextColor, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); + GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); + GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.TextColorNormal, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); - GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), "-100", GUI.Style.TextColor, font: GUI.SmallFont); - Vector2 textSize = GUI.SmallFont.MeasureString("100"); - GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), "100", GUI.Style.TextColor, font: GUI.SmallFont); + GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), "-100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); + Vector2 textSize = GUIStyle.SmallFont.MeasureString("100"); + GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), "100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs deleted file mode 100644 index fd2e7a7cd..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ /dev/null @@ -1,1932 +0,0 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; -using OpenAL; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma -{ - public partial class GameSettings - { - public enum Tab - { - Graphics, - Audio, - VoiceChat, - Controls, - Gameplay, -#if DEBUG - Debug -#endif - } - - private readonly Point MinSupportedResolution = new Point(1024, 540); - - private GUIFrame settingsFrame; - private GUIButton applyButton; - - private GUIFrame[] tabs; - private GUIButton[] tabButtons; - - public Action OnHUDScaleChanged; - - public GUIFrame SettingsFrame - { - get - { - if (settingsFrame == null) CreateSettingsFrame(); - return settingsFrame; - } - } - - private const int inventoryHotkeyCount = 10; - - public void SetDefaultBindings(XDocument doc = null, bool legacy = false) - { - keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; - keyMapping[(int)InputType.Run] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.F); - keyMapping[(int)InputType.Crouch] = new KeyOrMouse(Keys.LeftControl); - keyMapping[(int)InputType.Grab] = new KeyOrMouse(Keys.G); - keyMapping[(int)InputType.Health] = new KeyOrMouse(Keys.H); - keyMapping[(int)InputType.Ragdoll] = new KeyOrMouse(Keys.Space); - keyMapping[(int)InputType.Aim] = new KeyOrMouse(MouseButton.SecondaryMouse); - - keyMapping[(int)InputType.InfoTab] = new KeyOrMouse(Keys.Tab); - keyMapping[(int)InputType.Chat] = new KeyOrMouse(Keys.T); - keyMapping[(int)InputType.RadioChat] = new KeyOrMouse(Keys.R); - keyMapping[(int)InputType.CrewOrders] = new KeyOrMouse(Keys.C); - - keyMapping[(int)InputType.Voice] = new KeyOrMouse(Keys.V); - keyMapping[(int)InputType.LocalVoice] = new KeyOrMouse(Keys.B); - keyMapping[(int)InputType.Command] = new KeyOrMouse(MouseButton.MiddleMouse); - keyMapping[(int)InputType.PreviousFireMode] = new KeyOrMouse(MouseButton.MouseWheelDown); - keyMapping[(int)InputType.NextFireMode] = new KeyOrMouse(MouseButton.MouseWheelUp); - - keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); - - if (Language == "French") - { - keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.Z); - keyMapping[(int)InputType.Down] = new KeyOrMouse(Keys.S); - keyMapping[(int)InputType.Left] = new KeyOrMouse(Keys.Q); - keyMapping[(int)InputType.Right] = new KeyOrMouse(Keys.D); - keyMapping[(int)InputType.ToggleInventory] = new KeyOrMouse(Keys.A); - - keyMapping[(int)InputType.SelectNextCharacter] = new KeyOrMouse(Keys.X); - keyMapping[(int)InputType.SelectPreviousCharacter] = new KeyOrMouse(Keys.W); - } - else - { - keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.W); - keyMapping[(int)InputType.Down] = new KeyOrMouse(Keys.S); - keyMapping[(int)InputType.Left] = new KeyOrMouse(Keys.A); - keyMapping[(int)InputType.Right] = new KeyOrMouse(Keys.D); - keyMapping[(int)InputType.ToggleInventory] = new KeyOrMouse(Keys.Q); - - keyMapping[(int)InputType.SelectNextCharacter] = new KeyOrMouse(Keys.Z); - keyMapping[(int)InputType.SelectPreviousCharacter] = new KeyOrMouse(Keys.X); - } - - if (legacy) - { - keyMapping[(int)InputType.Use] = new KeyOrMouse(MouseButton.PrimaryMouse); - keyMapping[(int)InputType.Shoot] = new KeyOrMouse(MouseButton.PrimaryMouse); - keyMapping[(int)InputType.Select] = new KeyOrMouse(Keys.E); - keyMapping[(int)InputType.Deselect] = new KeyOrMouse(Keys.E); - } - else - { - keyMapping[(int)InputType.Use] = new KeyOrMouse(Keys.E); - keyMapping[(int)InputType.Select] = new KeyOrMouse(MouseButton.PrimaryMouse); - // shoot and deselect are handled in CheckBindings() so that we don't override the legacy settings. - } - - inventoryKeyMapping = new KeyOrMouse[inventoryHotkeyCount]; - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - inventoryKeyMapping[i] = new KeyOrMouse(Keys.D0 + (i + 1) % 10); - } - - if (doc != null) - { - LoadControls(doc); - } - } - - public void CheckBindings(bool useDefaults) - { - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - var binding = keyMapping[(int)inputType]; - if (binding == null) - { - switch (inputType) - { - case InputType.Deselect: - if (useDefaults) - { - binding = new KeyOrMouse(MouseButton.SecondaryMouse); - } - else - { - // Legacy support - var selectKey = keyMapping[(int)InputType.Select]; - if (selectKey != null && selectKey.Key != Keys.None) - { - binding = new KeyOrMouse(selectKey.Key); - } - } - break; - case InputType.Shoot: - if (useDefaults) - { - binding = new KeyOrMouse(MouseButton.PrimaryMouse); - } - else - { - // Legacy support - var useKey = keyMapping[(int)InputType.Use]; - if (useKey != null && useKey.MouseButton != MouseButton.None) - { - binding = new KeyOrMouse(useKey.MouseButton); - } - } - break; - default: - break; - } - if (binding == null) - { - DebugConsole.ThrowError("Key binding for the input type \"" + inputType + " not set!"); - binding = new KeyOrMouse(Keys.D1); - } - keyMapping[(int)inputType] = binding; - } - } - } - - private void LoadKeyBinds(XElement element, Version gameVersion) - { - foreach (XAttribute attribute in element.Attributes()) - { - //backwards compatibility - if (attribute.Name.ToString() == "TakeAllFromInventorySlot") - { - keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); - } - if (!Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType)) { continue; } - - if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) - { - keyMapping[(int)inputType] = new KeyOrMouse((MouseButton)mouseButtonInt); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out MouseButton mouseButton)) - { - keyMapping[(int)inputType] = new KeyOrMouse(mouseButton); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) - { - keyMapping[(int)inputType] = new KeyOrMouse(key); - } - } - //v0.15 added creature attacks that can be used with a character capable of speaking (with mudraptor or spineling genes), - //which causes the previous attack keybind R to conflict with the radio keybind - // -> automatically change it to F - if (gameVersion < new Version(0, 15, 0, 0)) - { - keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.F); - } - } - - private void LoadInventoryKeybinds(XElement element) - { - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - XAttribute attribute = element.Attributes().ElementAt(i); - if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) - { - inventoryKeyMapping[i] = new KeyOrMouse((MouseButton)mouseButtonInt); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out MouseButton mouseButton)) - { - inventoryKeyMapping[i] = new KeyOrMouse(mouseButton); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) - { - inventoryKeyMapping[i] = new KeyOrMouse(key); - } - } - } - - private void LoadControls(XDocument doc) - { - var gameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0")); - - XElement keyMapping = doc.Root.Element("keymapping"); - if (keyMapping != null) - { - LoadKeyBinds(keyMapping, gameVersion); - } - - XElement inventoryKeyMapping = doc.Root.Element("inventorykeymapping"); - if (inventoryKeyMapping != null) - { - LoadInventoryKeybinds(inventoryKeyMapping); - } - - XElement debugConsoleMapping = doc.Root.Element("debugconsolemapping"); - - if (debugConsoleMapping == null) { return; } - - ConsoleKeybinds.Clear(); - DebugConsole.Keybinds.Clear(); - - foreach (XElement element in debugConsoleMapping.Elements()) - { - string keyString = element.GetAttributeString("key", string.Empty); - string command = element.GetAttributeString("command", string.Empty); - - if (string.IsNullOrWhiteSpace(keyString) || string.IsNullOrWhiteSpace(command)) { continue; } - - if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object @out) && @out is Keys key) - { - ConsoleKeybinds.TryAdd(key, command); - } - } - - DebugConsole.Keybinds = new Dictionary(ConsoleKeybinds); - } - - private void LoadSubEditorImages(XDocument doc) - { - XElement element = doc.Root?.Element("editorimages"); - if (element == null) - { - SubEditorScreen.ImageManager.Clear(alsoPending: true); - return; - } - - SubEditorScreen.ImageManager.Load(element); - } - - public KeyOrMouse KeyBind(InputType inputType) - { - return keyMapping[(int)inputType]; - } - - public string KeyBindText(InputType inputType) - { - return keyMapping[(int)inputType].Name; - } - - public KeyOrMouse InventoryKeyBind(int index) - { - return inventoryKeyMapping[index]; - } - - private GUIListBox contentPackageList; - - private bool ChangeSliderText(GUIScrollBar scrollBar, float scale) - { - UnsavedSettings = true; - GUITextBlock text = scrollBar.UserData as GUITextBlock; - //search for percentage value - int index = text.Text.IndexOf("%"); - string label = text.Text; - //if "%" is found - if (index > 0) - { - while (index > 0) - { - //search for end of label - index -= 1; - if (text.Text[index] == ' ') - break; - } - label = text.Text.Substring(0, index); - } - text.Text = label + " " + (int)Math.Round(scale * 100) + "%"; - return true; - } - - public void ResetSettingsFrame() - { - if (GameMain.Client == null) - { - VoipCapture.Instance?.Dispose(); - } - settingsFrame = null; - } - - public void CreateSettingsFrame(Tab selectedTab = Tab.Graphics) - { - RectTransform settingsHolder = null; - - if (Screen.Selected == GameMain.MainMenuScreen) - { - settingsFrame = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), GUI.Canvas, Anchor.Center)); - settingsHolder = settingsFrame.RectTransform; - } - else - { - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - var settingsFrameContent = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), settingsFrame.RectTransform, Anchor.Center)); - settingsHolder = settingsFrameContent.RectTransform; - } - - Vector2 textBlockScale = new Vector2(1.0f, 0.05f); - Vector2 tickBoxScale = new Vector2(1.0f, 0.05f); - - var settingsFramePadding = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.93f), settingsHolder, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }, style: null); - var buttonArea = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.06f), settingsFramePadding.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.07f, 0.0f) }, style: null) - { - IgnoreLayoutGroups = true - }; - - /// General tab -------------------------------------------------------------- - - var leftPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 0.93f), settingsFramePadding.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - var settingsTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), - TextManager.Get("Settings"), textAlignment: Alignment.TopLeft, font: GUI.LargeFont) - { ForceUpperCase = true }; - - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), settingsTitle.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), style: "GUIBugButton") - { - ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})", - OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; } - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), TextManager.Get("ContentPackages"), font: GUI.SubHeadingFont); - - var corePackageDropdown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform)) - { - ButtonEnabled = ContentPackage.CorePackages.Count > 1 - }; - - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), isHorizontal: true) - { - Stretch = true - }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => - { - foreach (GUIComponent child in contentPackageList.Content.Children) - { - if (!(child.UserData is ContentPackage cp)) { continue; } - child.Visible = string.IsNullOrEmpty(text) ? true : cp.Name.ToLower().Contains(text.ToLower()); - } - return true; - }; - - contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.70f), leftPanel.RectTransform)) - { - OnSelected = (gc, obj) => false, - ScrollBarVisible = true - }; - - foreach (ContentPackage contentPackage in ContentPackage.CorePackages) - { - var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), corePackageDropdown.ListBox.Content.RectTransform), style: "ListBoxElement") - { - UserData = contentPackage - }; - var text = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform), contentPackage.Name); - - if (!contentPackage.IsCompatible()) - { - frame.UserData = null; - text.TextColor = GUI.Style.Red * 0.6f; - frame.ToolTip = text.ToolTip = - TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { contentPackage.Name, contentPackage.GameVersion.ToString(), GameMain.Version.ToString() }); - } - else if (!contentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) - { - frame.UserData = null; - text.TextColor = GUI.Style.Red * 0.6f; - frame.ToolTip = text.ToolTip = - TextManager.GetWithVariables("ContentPackageMissingCoreFiles", new string[2] { "[packagename]", "[missingfiletypes]" }, - new string[2] { contentPackage.Name, string.Join(", ", missingContentTypes) }, new bool[2] { false, true }); - } - else if (contentPackage.HasErrors) - { - text.TextColor = new Color(255, 150, 150); - frame.ToolTip = text.ToolTip = - TextManager.GetWithVariable("ContentPackageHasErrors", "[packagename]", contentPackage.Name) + - "\n" + string.Join("\n", contentPackage.ErrorMessages); - } - - if (contentPackage == CurrentCorePackage) - { - corePackageDropdown.Select(corePackageDropdown.ListBox.Content.GetChildIndex(frame)); - } - } - corePackageDropdown.OnSelected = SelectCorePackage; - corePackageDropdown.ListBox.CanBeFocused = CanHotswapPackages(true); - - foreach (ContentPackage contentPackage in ContentPackage.RegularPackages) - { - var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, tickBoxScale.Y), contentPackageList.Content.RectTransform), style: "ListBoxElement") - { - UserData = contentPackage - }; - - var frameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), - style: "GUIDragIndicator") - { - CanBeFocused = false - }; - var tickBox = new GUITickBox(new RectTransform(Vector2.One, frameContent.RectTransform), contentPackage.Name, - style: "GUITickBox") - { - UserData = contentPackage, - Selected = EnabledRegularPackages.Contains(contentPackage), - OnSelected = SelectContentPackage, - Enabled = CanHotswapPackages(false) - }; - frame.RectTransform.MinSize = new Point(0, (int)(tickBox.RectTransform.MinSize.Y / frameContent.RectTransform.RelativeSize.Y)); - if (!contentPackage.IsCompatible()) - { - tickBox.Enabled = false; - tickBox.TextColor = GUI.Style.Red * 0.6f; - tickBox.ToolTip = tickBox.TextBlock.ToolTip = - TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { contentPackage.Name, contentPackage.GameVersion.ToString(), GameMain.Version.ToString() }); - } - else if (contentPackage.HasErrors) - { - tickBox.TextColor = new Color(255,150,150); - tickBox.ToolTip = tickBox.TextBlock.ToolTip = - TextManager.GetWithVariable("ContentPackageHasErrors", "[packagename]", contentPackage.Name) + - "\n" + string.Join("\n", contentPackage.ErrorMessages); - } - } - contentPackageList.CurrentDragMode = CanHotswapPackages(false) ? GUIListBox.DragMode.DragWithinBox : GUIListBox.DragMode.NoDragging; - contentPackageList.CanBeFocused = CanHotswapPackages(false); - contentPackageList.OnRearranged = OnContentPackagesRearranged; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform), TextManager.Get("Language"), font: GUI.SubHeadingFont); - var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform)); - foreach (string language in TextManager.AvailableLanguages) - { - languageDD.AddItem(TextManager.GetTranslatedLanguageName(language), language); - } - languageDD.SelectItem(TextManager.Language); - languageDD.OnSelected = (guiComponent, obj) => - { - string newLanguage = obj as string; - if (newLanguage == Language) { return true; } - - string prevLanguage = Language; - Language = newLanguage; - UnsavedSettings = true; - - var msgBox = new GUIMessageBox( - TextManager.Get("RestartRequiredLabel"), - TextManager.Get("RestartRequiredLanguage"), - buttons: new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (btn, userdata) => - { - ApplySettings(); - GameMain.Instance.Exit(); - return true; - }; - msgBox.Buttons[1].OnClicked += (btn, userdata) => - { - Language = prevLanguage; - languageDD.SelectItem(Language); - msgBox.Close(); - return true; - }; - return true; - }; - -#if !OSX - var statisticsTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform), TextManager.Get("statisticsconsenttickbox")) - { - OnSelected = (GUITickBox tickBox) => - { - GameAnalyticsManager.SetConsent( - tickBox.Selected - ? GameAnalyticsManager.Consent.Ask - : GameAnalyticsManager.Consent.No); - return false; - } - }; -#if DEBUG - statisticsTickBox.Enabled = false; -#endif - void updateGATickBoxToolTip() - => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); - updateGATickBoxToolTip(); - - var cachedConsent = GameAnalyticsManager.Consent.Unknown; - var statisticsTickBoxUpdater = new GUICustomComponent( - new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform), - onUpdate: (deltaTime, component) => - { - bool shouldTickBoxBeSelected = GameAnalyticsManager.UserConsented == GameAnalyticsManager.Consent.Yes; - - bool shouldUpdateTickBoxState = cachedConsent != GameAnalyticsManager.UserConsented - || statisticsTickBox.Selected != shouldTickBoxBeSelected; - - if (!shouldUpdateTickBoxState) { return; } - - updateGATickBoxToolTip(); - cachedConsent = GameAnalyticsManager.UserConsented; - GUITickBox.OnSelectedHandler prevHandler = statisticsTickBox.OnSelected; - statisticsTickBox.OnSelected = null; - statisticsTickBox.Selected = shouldTickBoxBeSelected; - statisticsTickBox.OnSelected = prevHandler; - statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; - }); -#endif - - foreach (var child in leftPanel.Children) - { - if (child is GUITextBlock textBlock) - { - textBlock.RectTransform.MinSize = new Point(textBlock.RectTransform.MinSize.X, (int)Math.Max(textBlock.RectTransform.MinSize.Y, textBlock.TextSize.Y)); - } - } - - // right panel -------------------------------------- - - var rightPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.99f - leftPanel.RectTransform.RelativeSize.X, leftPanel.RectTransform.RelativeSize.Y), - settingsFramePadding.RectTransform, Anchor.TopRight)) - { - Stretch = true - }; - - var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightPanel.RectTransform, Anchor.TopCenter), isHorizontal: true); - - var paddedFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), rightPanel.RectTransform, Anchor.Center), style: null); - - tabs = new GUIFrame[Enum.GetValues(typeof(Tab)).Length]; - tabButtons = new GUIButton[tabs.Length]; - foreach (Tab tab in Enum.GetValues(typeof(Tab))) - { - tabs[(int)tab] = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), paddedFrame.RectTransform), style: "InnerFrame") - { - UserData = tab - }; - - float tabWidth = 1.0f / tabs.Length; -#if DEBUG - string buttonText = tab != Tab.Debug ? TextManager.Get("SettingsTab." + tab.ToString()) : "Debug"; -#else - string buttonText = TextManager.Get("SettingsTab." + tab.ToString()); -#endif - - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), style: "GUITabButton") - { - UserData = tab, - OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } - }; - tabButtons[(int)tab].Text = ToolBox.LimitString(buttonText, tabButtons[(int)tab].Font, (int)(0.75f * tabWidth * tabButtonHolder.Rect.Width)); - if (tabButtons[(int)tab].Text != buttonText) - { - tabButtons[(int)tab].ToolTip = buttonText; - } - } - - /// Graphics tab -------------------------------------------------------------- - - var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Graphics].RectTransform, Anchor.TopLeft) - { RelativeOffset = new Vector2(0.025f, 0.02f) }) - { RelativeSpacing = 0.01f }; - var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Graphics].RectTransform, Anchor.TopRight) - { RelativeOffset = new Vector2(0.025f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("Resolution"), font: GUI.SubHeadingFont); - var resolutionDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform)) - { - ButtonEnabled = GameMain.Config.WindowMode != WindowMode.BorderlessWindowed - }; - - var supportedDisplayModes = UpdateResolutionDD(resolutionDD); - resolutionDD.OnSelected = SelectResolution; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("DisplayMode"), font: GUI.SubHeadingFont); - var displayModeDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform)); - - displayModeDD.AddItem(TextManager.Get("Fullscreen"), WindowMode.Fullscreen); - displayModeDD.AddItem(TextManager.Get("Windowed"), WindowMode.Windowed); -#if (!OSX) - displayModeDD.AddItem(TextManager.Get("BorderlessWindowed"), WindowMode.BorderlessWindowed); - displayModeDD.SelectItem(GameMain.Config.WindowMode); -#else - // Fullscreen option will just set itself to borderless on macOS. - if (GameMain.Config.WindowMode == WindowMode.BorderlessWindowed) - { - displayModeDD.SelectItem(WindowMode.Fullscreen); - } - else - { - displayModeDD.SelectItem(GameMain.Config.WindowMode); - } -#endif - - displayModeDD.OnSelected = (guiComponent, obj) => - { - UnsavedSettings = true; - GameMain.Config.WindowMode = (WindowMode)guiComponent.UserData; - supportedDisplayModes = UpdateResolutionDD(resolutionDD); - resolutionDD.ButtonEnabled = GameMain.Config.WindowMode != WindowMode.BorderlessWindowed; - GameMain.Instance.ApplyGraphicsSettings(); - if (GameMain.Config.WindowMode == WindowMode.BorderlessWindowed) - { - GraphicsWidth = GameMain.GraphicsWidth; - GraphicsHeight = GameMain.GraphicsHeight; - var displayMode = supportedDisplayModes.Find(m => m.Width == GameMain.GraphicsWidth && m.Height == GameMain.GraphicsHeight); - if (displayMode != null) - { - resolutionDD.SelectItem(displayMode); - } - } - return true; - }; - - GUITickBox vsyncTickBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("EnableVSync")) - { - ToolTip = TextManager.Get("EnableVSyncToolTip"), - Selected = VSyncEnabled - }; - vsyncTickBox.OnSelected = (GUITickBox box) => - { - VSyncEnabled = box.Selected; - GameMain.GraphicsDeviceManager.SynchronizeWithVerticalRetrace = VSyncEnabled; - GameMain.GraphicsDeviceManager.ApplyChanges(); - UnsavedSettings = true; - - return true; - }; - - - GUITickBox textureCompressionTickBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("EnableTextureCompression")) - { - ToolTip = TextManager.Get("EnableTextureCompressionToolTip"), - OnSelected = (GUITickBox box) => - { - if (box.Selected == TextureCompressionEnabled) { return true; } - bool prevTextureCompressionEnabled = TextureCompressionEnabled; - TextureCompressionEnabled = box.Selected; - - var msgBox = new GUIMessageBox( - TextManager.Get("RestartRequiredLabel"), - TextManager.Get("RestartRequiredGeneric"), - buttons: new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (btn, userdata) => - { - ApplySettings(); - GameMain.Instance.Exit(); - return true; - }; msgBox.Buttons[1].OnClicked += (btn, userdata) => - { - TextureCompressionEnabled = prevTextureCompressionEnabled; - box.Selected = prevTextureCompressionEnabled; - msgBox.Close(); - return true; - }; - - return true; - }, - Selected = TextureCompressionEnabled - }; - - GUITextBlock particleLimitText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("ParticleLimit"), font: GUI.SubHeadingFont, wrap: true); - GUIScrollBar particleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), style: "GUISlider", - barSize: 0.1f) - { - UserData = particleLimitText, - BarScroll = (ParticleLimit - 200) / 1300.0f, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - ParticleLimit = 200 + (int)(scroll * 1300.0f); - return true; - }, - Step = 0.1f - }; - particleScrollBar.OnMoved(particleScrollBar, particleScrollBar.BarScroll); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("LosEffect"), font: GUI.SubHeadingFont, wrap: true); - var losModeDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform)); - losModeDD.AddItem(TextManager.Get("LosModeNone"), LosMode.None); - losModeDD.AddItem(TextManager.Get("LosModeTransparent"), LosMode.Transparent); - losModeDD.AddItem(TextManager.Get("LosModeOpaque"), LosMode.Opaque); - losModeDD.SelectItem(GameMain.Config.LosMode); - losModeDD.OnSelected = (guiComponent, obj) => - { - UnsavedSettings = true; - GameMain.Config.LosMode = (LosMode)guiComponent.UserData; - //don't allow changing los mode when playing as a client - if (GameMain.Client == null) - { - GameMain.LightManager.LosMode = GameMain.Config.LosMode; - } - return true; - }; - - GUITextBlock LightText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("LightMapScale"), font: GUI.SubHeadingFont, wrap: true) - { - ToolTip = TextManager.Get("LightMapScaleToolTip") - }; - GUIScrollBar lightScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = LightText, - ToolTip = TextManager.Get("LightMapScaleToolTip"), - BarScroll = MathUtils.InverseLerp(0.2f, 1.0f, LightMapScale), - OnMoved = (scrollBar, barScroll) => - { - ChangeSliderText(scrollBar, barScroll); - LightMapScale = MathHelper.Lerp(0.2f, 1.0f, barScroll); - UnsavedSettings = true; - return true; - }, - Step = 0.25f - }; - lightScrollBar.OnMoved(lightScrollBar, lightScrollBar.BarScroll); - - /*new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("SpecularLighting")) - { - ToolTip = TextManager.Get("SpecularLightingToolTip"), - Selected = SpecularityEnabled, - OnSelected = (tickBox) => - { - SpecularityEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - };*/ - - new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("RadialDistortion")) - { - ToolTip = TextManager.Get("RadialDistortionToolTip"), - Selected = EnableRadialDistortion, - OnSelected = (tickBox) => - { - EnableRadialDistortion = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("ChromaticAberration")) - { - ToolTip = TextManager.Get("ChromaticAberrationToolTip"), - Selected = ChromaticAberrationEnabled, - OnSelected = (tickBox) => - { - ChromaticAberrationEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - /// Audio tab ---------------------------------------------------------------- - - var audioContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.Audio].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = false, - RelativeSpacing = 0.01f - }; - -#if (!OSX) - AudioDeviceNames = Alc.GetStringList((IntPtr)null, Alc.AllDevicesSpecifier); - if (string.IsNullOrEmpty(AudioOutputDevice)) - { - AudioOutputDevice = Alc.GetString((IntPtr)null, Alc.DefaultDeviceSpecifier); - if (AudioDeviceNames.Any() && !AudioDeviceNames.Any(n => n.Equals(AudioOutputDevice, StringComparison.OrdinalIgnoreCase))) - { - AudioOutputDevice = AudioDeviceNames[0]; - } - } - - var outputDeviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), audioContent.RectTransform), TrimAudioDeviceName(AudioOutputDevice), AudioDeviceNames.Count); - if (AudioDeviceNames?.Count > 0) - { - foreach (string name in AudioDeviceNames) - { - outputDeviceList.AddItem(TrimAudioDeviceName(name), name); - } - outputDeviceList.OnSelected = (GUIComponent selected, object obj) => - { - string name = obj as string; - if (!GameMain.SoundManager.Disconnected && AudioOutputDevice == name) { return true; } - - AudioOutputDevice = name; - GameMain.SoundManager.InitializeAlcDevice(AudioOutputDevice); - - return true; - }; - } - else - { - outputDeviceList.AddItem(TextManager.Get("AudioNoDevices") ?? "N/A", null); - outputDeviceList.ButtonTextColor = GUI.Style.Red; - outputDeviceList.ButtonEnabled = false; - outputDeviceList.Select(0); - } -#endif - - GUITextBlock soundVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("SoundVolume"), font: GUI.SubHeadingFont); - GUIScrollBar soundScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = soundVolumeText, - BarScroll = SoundVolume, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - SoundVolume = scroll; - return true; - } - }; - soundScrollBar.OnMoved(soundScrollBar, soundScrollBar.BarScroll); - - GUITextBlock musicVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("MusicVolume"), font: GUI.SubHeadingFont); - GUIScrollBar musicScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = musicVolumeText, - BarScroll = MusicVolume, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - MusicVolume = scroll; - return true; - } - }; - musicScrollBar.OnMoved(musicScrollBar, musicScrollBar.BarScroll); - - GUITextBlock voiceChatVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("VoiceChatVolume"), font: GUI.SubHeadingFont); - GUIScrollBar voiceChatScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = voiceChatVolumeText, - Range = new Vector2(0.0f, 2.0f) - }; - voiceChatScrollBar.BarScrollValue = VoiceChatVolume; - voiceChatScrollBar.OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scrollBar.BarScrollValue); - VoiceChatVolume = scrollBar.BarScrollValue; - return true; - }; - voiceChatScrollBar.OnMoved(voiceChatScrollBar, voiceChatScrollBar.BarScroll); - - GUITickBox muteOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("MuteOnFocusLost")) - { - Selected = MuteOnFocusLost, - ToolTip = TextManager.Get("MuteOnFocusLostToolTip"), - OnSelected = (tickBox) => - { - MuteOnFocusLost = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - GUITickBox dynamicRangeCompressionTickBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("DynamicRangeCompression")) - { - Selected = DynamicRangeCompressionEnabled, - ToolTip = TextManager.Get("DynamicRangeCompressionToolTip"), - OnSelected = (tickBox) => - { - DynamicRangeCompressionEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - GUITickBox voipAttenuationTickBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("VoipAttenuation")) - { - Selected = VoipAttenuationEnabled, - ToolTip = TextManager.Get("VoipAttenuationToolTip"), - OnSelected = (tickBox) => - { - VoipAttenuationEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - /// Voice chat tab ---------------------------------------------------------------- - - var voiceChatContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.VoiceChat].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = false, - RelativeSpacing = 0.01f - }; - - //new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("VoiceChat"), font: GUI.SubHeadingFont); - - CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); - foreach (string name in CaptureDeviceNames) - { - DebugConsole.NewMessage(name + " " + name.Length.ToString(), Color.Lime); - } - - GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale, voiceChatContent.RectTransform), TextManager.Get("DirectionalVoiceChat")) - { - Selected = UseDirectionalVoiceChat, - ToolTip = TextManager.Get("DirectionalVoiceChatToolTip"), - OnSelected = (tickBox) => - { - UseDirectionalVoiceChat = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - if (string.IsNullOrWhiteSpace(VoiceCaptureDevice) || !(CaptureDeviceNames?.Contains(VoiceCaptureDevice) ?? false)) - { - VoiceCaptureDevice = CaptureDeviceNames?.Count > 0 ? CaptureDeviceNames[0] : null; - } - if (string.IsNullOrWhiteSpace(VoiceCaptureDevice)) - { - VoiceSetting = VoiceMode.Disabled; - } -#if (!OSX) - var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), CaptureDeviceNames.Count); - if (CaptureDeviceNames?.Count > 0) - { - foreach (string name in CaptureDeviceNames) - { - deviceList.AddItem(TrimAudioDeviceName(name), name); - } - deviceList.OnSelected = (GUIComponent selected, object obj) => - { - string name = obj as string; - if (!(VoipCapture.Instance?.Disconnected ?? true) && VoiceCaptureDevice == name) { return true; } - - VoipCapture.ChangeCaptureDevice(name); - return true; - }; - } - else - { - deviceList.AddItem(TextManager.Get("VoipNoDevices") ?? "N/A", null); - deviceList.ButtonTextColor = GUI.Style.Red; - deviceList.ButtonEnabled = false; - deviceList.Select(0); - } - -#else - var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), voiceChatContent.RectTransform), true, Anchor.CenterLeft); - var currentDeviceTextBlock = new GUITextBlock(new RectTransform(new Vector2(.7f, 0.75f), null), - TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice)), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("CurrentDeviceToolTip.OSX"), - TextAlignment = Alignment.CenterLeft - }; - - string refreshText = ToolBox.WrapText(TextManager.Get("RefreshDefaultDevice"), defaultDeviceGroup.RectTransform.Rect.Width * 0.3f, GUI.Font); - var currentDeviceButton = new GUIButton(new RectTransform(new Vector2(.3f, 0.75f), defaultDeviceGroup.RectTransform), refreshText) - { - ToolTip = TextManager.Get("RefreshDefaultDeviceToolTip"), - OnClicked = (bt, userdata) => - { - CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); - if (CaptureDeviceNames?.Count > 0) - { - if (VoiceCaptureDevice == CaptureDeviceNames[0]) return true; - - VoipCapture.ChangeCaptureDevice(CaptureDeviceNames[0]); - currentDeviceTextBlock.Text = TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice)); - currentDeviceTextBlock.Flash(Color.Blue); - } - else - { - currentDeviceTextBlock.Text = TextManager.Get("VoipNoDevices") ?? "N/A"; - currentDeviceTextBlock.Flash(GUI.Style.Red); - } - - return true; - } - }; - currentDeviceButton.OnClicked(currentDeviceButton, null); - - currentDeviceTextBlock.RectTransform.Parent = defaultDeviceGroup.RectTransform; -#endif - - var voiceModeCount = Enum.GetNames(typeof(VoiceMode)).Length; - var voiceModeDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), elementCount: voiceModeCount); - for (int i = 0; i < voiceModeCount; i++) - { - var voiceMode = "VoiceMode." + ((VoiceMode)i).ToString(); - voiceModeDropDown.AddItem(TextManager.Get(voiceMode), userData: i, toolTip: TextManager.Get(voiceMode + "ToolTip")); - } - - var micVolumeText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("MicrophoneVolume"), font: GUI.SubHeadingFont); - var micVolumeSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = micVolumeText, - BarScroll = (float)Math.Sqrt(MathUtils.InverseLerp(0.2f, MaxMicrophoneVolume, MicrophoneVolume)), - OnMoved = (scrollBar, scroll) => - { - MicrophoneVolume = MathHelper.Lerp(0.2f, MaxMicrophoneVolume, scroll * scroll); - MicrophoneVolume = (float)Math.Round(MicrophoneVolume, 1); - ChangeSliderText(scrollBar, MicrophoneVolume); - scrollBar.Step = 0.05f; - return true; - }, - Step = 0.05f - }; - micVolumeSlider.OnMoved(micVolumeSlider, micVolumeSlider.BarScroll); - - var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), voiceChatContent.RectTransform, Anchor.BottomCenter), style: null); - - var voiceActivityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform)) - { - Visible = VoiceSetting != VoiceMode.Disabled - }; - GUITickBox localVoiceByDefault = new GUITickBox( - new RectTransform(tickBoxScale, voiceActivityGroup.RectTransform), TextManager.Get("LocalVoiceByDefault")) - { - Visible = VoiceSetting == VoiceMode.Activity, - Selected = UseLocalVoiceByDefault, - ToolTip = TextManager.Get("LocalVoiceByDefaultTooltip"), - OnSelected = (tickBox) => - { - UseLocalVoiceByDefault = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - GUITextBlock noiseGateText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), voiceActivityGroup.RectTransform), TextManager.Get("NoiseGateThreshold"), font: GUI.SubHeadingFont) - { - Visible = VoiceSetting == VoiceMode.Activity, - TextGetter = () => - { - return TextManager.Get("NoiseGateThreshold") + " " + ((int)NoiseGateThreshold).ToString() + " dB"; - } - }; - var dbMeter = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.5f), voiceActivityGroup.RectTransform), 0.0f, Color.Lime); - dbMeter.ProgressGetter = () => - { - if (VoipCapture.Instance == null) { return 0.0f; } - - if (VoiceSetting == VoiceMode.Activity) - { - dbMeter.Color = VoipCapture.Instance.LastdB > NoiseGateThreshold ? GUI.Style.Green : GUI.Style.Orange; //TODO: i'm a filthy hack - } - else - { - dbMeter.Color = Color.Lime; - } - - float scrollVal = double.IsNegativeInfinity(VoipCapture.Instance.LastdB) ? 0.0f : ((float)VoipCapture.Instance.LastdB + 100.0f) / 100.0f; - return scrollVal * scrollVal; - }; - var noiseGateSlider = new GUIScrollBar(new RectTransform(Vector2.One, dbMeter.RectTransform, Anchor.Center), color: Color.White, - style: "GUISlider", barSize: 0.03f); - noiseGateSlider.Frame.Visible = false; - noiseGateSlider.Step = 0.01f; - noiseGateSlider.Range = new Vector2(-100.0f, 0.0f); - noiseGateSlider.BarScroll = MathUtils.InverseLerp(-100.0f, 0.0f, NoiseGateThreshold); - noiseGateSlider.BarScroll *= noiseGateSlider.BarScroll; - noiseGateSlider.Visible = VoiceSetting == VoiceMode.Activity; - noiseGateSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - NoiseGateThreshold = MathHelper.Lerp(-100.0f, 0.0f, (float)Math.Sqrt(scrollBar.BarScroll)); - UnsavedSettings = true; - return true; - }; - - var voiceInputContainerHorizontal = new GUILayoutGroup( - new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform) - { - RelativeOffset = new Vector2(0.0f, voiceActivityGroup.RectTransform.RelativeSize.Y + 0.1f) - }, - isHorizontal: true) - { - Visible = VoiceSetting == VoiceMode.PushToTalk - }; - - var voiceInputContainer = new GUILayoutGroup( - new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft); - - var voiceKeybindLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice"), font: GUI.SubHeadingFont); - var voiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1.0f), voiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.Voice)) - { - SelectedColor = Color.Gold * 0.3f, - UserData = InputType.Voice - }; - voiceKeyBox.OnSelected += KeyBoxSelected; - - var localVoiceInputContainer = new GUILayoutGroup( - new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft); - - var localVoiceKeybindLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), localVoiceInputContainer.RectTransform), TextManager.Get("InputType.LocalVoice"), font: GUI.SubHeadingFont); - var localVoiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1.0f), localVoiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.LocalVoice)) - { - SelectedColor = Color.Gold * 0.3f, - UserData = InputType.LocalVoice - }; - localVoiceKeyBox.OnSelected += KeyBoxSelected; - - voiceKeybindLabel.RectTransform.SizeChanged += () => { GUITextBlock.AutoScaleAndNormalize(voiceKeybindLabel, localVoiceKeybindLabel); }; - - var cutoffPreventionText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("CutoffPrevention"), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("CutoffPreventionTooltip") - }; - var cutoffPreventionSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = micVolumeText, - Range = new Vector2(0, ((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f * 25.0f), - Step = 1.0f / 25.0f - }; - cutoffPreventionSlider.BarScrollValue = VoiceChatCutoffPrevention; - cutoffPreventionSlider.OnMoved = (scrollBar, scroll) => - { - int bufferMsLength = (int)(((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f); - VoiceChatCutoffPrevention = (int)Math.Round(scrollBar.BarScrollValue / bufferMsLength) * bufferMsLength; - cutoffPreventionText.Text = TextManager.Get("CutoffPrevention") + - " " + TextManager.GetWithVariable("timeformatmilliseconds", "[milliseconds]", VoiceChatCutoffPrevention.ToString()); - return true; - }; - cutoffPreventionSlider.OnMoved(cutoffPreventionSlider, cutoffPreventionSlider.BarScrollValue); - - voiceModeDropDown.OnSelected = (GUIComponent selected, object userData) => - { - try - { - VoiceMode vMode = (VoiceMode)userData; - if (vMode == VoiceSetting) { return true; } - VoiceSetting = vMode; - if (vMode != VoiceMode.Disabled) - { - if (GameMain.Client == null && VoipCapture.Instance == null) - { - VoipCapture.Create(GameMain.Config.VoiceCaptureDevice); - if (VoipCapture.Instance == null) - { - VoiceSetting = vMode = VoiceMode.Disabled; - voiceActivityGroup.Visible = false; - voiceInputContainerHorizontal.Visible = false; - return true; - } - } - } - else - { - if (GameMain.Client == null) - { - VoipCapture.Instance?.Dispose(); - } - } - - noiseGateText.Visible = (vMode == VoiceMode.Activity); - noiseGateSlider.Visible = (vMode == VoiceMode.Activity); - localVoiceByDefault.Visible = (vMode == VoiceMode.Activity); - voiceActivityGroup.Visible = (vMode != VoiceMode.Disabled); - voiceInputContainerHorizontal.Visible = (vMode == VoiceMode.PushToTalk); - UnsavedSettings = true; - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to set voice capture mode.", e); - GameAnalyticsManager.AddErrorEventOnce("SetVoiceCaptureMode", GameAnalyticsManager.ErrorSeverity.Error, "Failed to set voice capture mode. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - VoiceSetting = VoiceMode.Disabled; - } - - return true; - }; - - voiceModeDropDown.Select((int)VoiceSetting); - if (string.IsNullOrWhiteSpace(VoiceCaptureDevice)) - { - voiceModeDropDown.ButtonEnabled = false; - voiceModeDropDown.Color *= 0.5f; - voiceModeDropDown.ButtonTextColor *= 0.5f; - } - - /// Controls tab ------------------------------------------------------------- - var controlsLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), tabs[(int)Tab.Controls].RectTransform, Anchor.TopCenter) - { RelativeOffset = new Vector2(0.0f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - GUITextBlock aimAssistText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), controlsLayoutGroup.RectTransform), TextManager.Get("AimAssist"), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("AimAssistToolTip") - }; - GUIScrollBar aimAssistSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), controlsLayoutGroup.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = aimAssistText, - BarScroll = MathUtils.InverseLerp(0.0f, 5.0f, AimAssistAmount), - ToolTip = TextManager.Get("AimAssistToolTip"), - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - AimAssistAmount = MathHelper.Lerp(0.0f, 5.0f, scroll); - return true; - }, - Step = 0.01f - }; - aimAssistSlider.OnMoved(aimAssistSlider, aimAssistSlider.BarScroll); - - new GUITickBox(new RectTransform(tickBoxScale, controlsLayoutGroup.RectTransform), TextManager.Get("EnableMouseLook")) - { - ToolTip = TextManager.Get("EnableMouseLookToolTip"), - Selected = EnableMouseLook, - OnSelected = (tickBox) => - { - EnableMouseLook = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - var controlListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), controlsLayoutGroup.RectTransform)); - - var inputFrame = new GUILayoutGroup(new RectTransform(Vector2.One, controlListBox.Content.RectTransform), isHorizontal: true) - { Stretch = true, RelativeSpacing = 0.01f }; - - var inputColumnLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), inputFrame.RectTransform)) - { Stretch = true, RelativeSpacing = 0.005f }; - var inputColumnRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), inputFrame.RectTransform)) - { Stretch = true, RelativeSpacing = 0.005f }; - - var inputNames = Enum.GetValues(typeof(InputType)); - var inputNameBlocks = new List(); - for (int i = 0; i < inputNames.Length; i++) - { - var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f),(i <= (inputNames.Length / 2) ? inputColumnLeft : inputColumnRight).RectTransform)) - { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215) }; - var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, - TextManager.Get("InputType." + ((InputType)i)), font: GUI.SmallFont) { ForceUpperCase = true }; - inputNameBlocks.Add(inputName); - string keyText = KeyBindText((InputType)i); - var keyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), inputContainer.RectTransform), - text: keyText, font: GUI.SmallFont, style: "GUITextBoxNoIcon") - { - UserData = i - }; - keyBox.RectTransform.SizeChanged += () => - { - keyBox.Text = ToolBox.LimitString(keyText, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); - }; - inputContainer.RectTransform.MinSize = keyBox.RectTransform.MinSize; - keyBox.OnSelected += KeyBoxSelected; - keyBox.SelectedColor = Color.Gold * 0.3f; - } - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), inputColumnRight.RectTransform, minSize: inputColumnRight.Children.First().RectTransform.MinSize), style: null); - - for (int i = 0; i < inventoryHotkeyCount; i++) - { - var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f), ((i + 1) <= inventoryHotkeyCount / 2 ? inputColumnLeft : inputColumnRight).RectTransform)) - { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215), CanBeFocused = true }; - var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, - TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (i + 1).ToString()), font: GUI.SmallFont) - { ForceUpperCase = true }; - inputNameBlocks.Add(inputName); - var keyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), inputContainer.RectTransform), - text: inventoryKeyMapping[i].Name, font: GUI.SmallFont, style: "GUITextBoxNoIcon") - { - UserData = i - }; - keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); - inputContainer.RectTransform.MinSize = keyBox.RectTransform.MinSize; - keyBox.OnSelected += InventoryKeyBoxSelected; - keyBox.SelectedColor = Color.Gold * 0.3f; - } - - inputNameBlocks.First().RectTransform.SizeChanged += () => - { - GUITextBlock.AutoScaleAndNormalize(inputNameBlocks); - }; - - inputFrame.RectTransform.MinSize = new Point(0, - (int)Math.Max( - inputColumnLeft.Children.Sum(c => c.Rect.Height * (1.0f + inputColumnLeft.RelativeSpacing)), - inputColumnRight.Children.Sum(c => c.Rect.Height * (1.0f + inputColumnLeft.RelativeSpacing)))); - - var resetControlsArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), controlsLayoutGroup.RectTransform), style: null); - var resetControlsHolder = new GUILayoutGroup(new RectTransform(new Vector2(buttonArea.RectTransform.RelativeSize.X / controlsLayoutGroup.RectTransform.RelativeSize.X / rightPanel.RectTransform.RelativeSize.X, 1.0f), resetControlsArea.RectTransform, Anchor.Center), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f - }; resetControlsHolder.CanBeFocused = true; - - var defaultBindingsButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), resetControlsHolder.RectTransform), TextManager.Get("SetDefaultBindings"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("SetDefaultBindingsToolTip"), - OnClicked = (button, data) => - { - ResetControls(legacy: false); - return true; - } - }; - - var legacyBindingsButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), resetControlsHolder.RectTransform), TextManager.Get("SetLegacyBindings"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("SetLegacyBindingsToolTip"), - OnClicked = (button, data) => - { - ResetControls(legacy: true); - return true; - } - }; - - legacyBindingsButton.TextBlock.RectTransform.SizeChanged += () => - { - GUITextBlock.AutoScaleAndNormalize(defaultBindingsButton.TextBlock, legacyBindingsButton.TextBlock); - }; - - /// Gameplay tab ------------------------------------------------------------- - var gameplaySettingsGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Gameplay].RectTransform, Anchor.TopLeft) - { RelativeOffset = new Vector2(0.025f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - GUITickBox pauseOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), - TextManager.Get("PauseOnFocusLost")) - { - Selected = PauseOnFocusLost, - ToolTip = TextManager.Get("PauseOnFocusLostToolTip"), - OnSelected = (tickBox) => - { - PauseOnFocusLost = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), TextManager.Get("DisableInGameHints")) - { - Selected = DisableInGameHints, - ToolTip = TextManager.Get("DisableInGameHintsToolTip"), - OnSelected = (tickBox) => - { - DisableInGameHints = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - text: TextManager.Get("ResetInGameHints"), - style: "GUIButtonSmall") - { - OnClicked = (button, userData) => - { - var msgBox = new GUIMessageBox(TextManager.Get("ResetInGameHints"), - TextManager.Get("ResetInGameHintsTooltip"), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (button, userData) => - { - GameMain.Config.IgnoredHints.Clear(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - }; - - GUITextBlock HUDScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("HUDScale"), font: GUI.SubHeadingFont, wrap: true); - GUIScrollBar HUDScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = HUDScaleText, - BarScroll = (HUDScale - MinHUDScale) / (MaxHUDScale - MinHUDScale), - OnMoved = (scrollBar, scroll) => - { - HUDScale = MathHelper.Lerp(MinHUDScale, MaxHUDScale, scroll); - ChangeSliderText(scrollBar, HUDScale); - OnHUDScaleChanged?.Invoke(); - return true; - }, - Step = 0.02f - }; - HUDScaleScrollBar.OnMoved(HUDScaleScrollBar, HUDScaleScrollBar.BarScroll); - - GUITextBlock inventoryScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("InventoryScale"), font: GUI.SubHeadingFont); - GUIScrollBar inventoryScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = inventoryScaleText, - BarScroll = (InventoryScale - MinInventoryScale) / (MaxInventoryScale - MinInventoryScale), - OnMoved = (scrollBar, scroll) => - { - InventoryScale = MathHelper.Lerp(MinInventoryScale, MaxInventoryScale, scroll); - ChangeSliderText(scrollBar, InventoryScale); - return true; - }, - Step = 0.02f - }; - inventoryScaleScrollBar.OnMoved(inventoryScaleScrollBar, inventoryScaleScrollBar.BarScroll); - - GUITextBlock textScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("TextScale"), font: GUI.SubHeadingFont); - GUIScrollBar textScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = textScaleText, - BarScroll = (TextScale - MinTextScale) / (MaxTextScale - MinTextScale), - OnMoved = (scrollBar, scroll) => - { - TextScale = MathHelper.Lerp(MinTextScale, MaxTextScale, scroll); - textScaleDirty = true; - ChangeSliderText(scrollBar, TextScale); - return true; - }, - Step = 0.01f - }; - textScaleScrollBar.OnMoved(textScaleScrollBar, textScaleScrollBar.BarScroll); - textScaleDirty = false; - - /// Bottom buttons ------------------------------------------------------------- - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), - TextManager.Get("Cancel")) - { - IgnoreLayoutGroups = true, - OnClicked = (x, y) => - { - static void ExitSettings() - { - if (Screen.Selected == GameMain.MainMenuScreen) { GameMain.MainMenuScreen.ReturnToMainMenu(null, null); } - GUI.SettingsMenuOpen = false; - } - - if (UnsavedSettings) - { - var msgBox = new GUIMessageBox(TextManager.Get("UnsavedChangesLabel"), - TextManager.Get("UnsavedChangesVerification"), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (applyButton, obj) => - { - LoadPlayerConfig(); - ExitSettings(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - - ExitSettings(); - return true; - } - }; - - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomCenter), - TextManager.Get("Reset")) - { - IgnoreLayoutGroups = true, - OnClicked = (button, data) => - { - var msgBox = new GUIMessageBox(TextManager.Get("SettingResetLabel"), - TextManager.Get("SettingResetVerification"), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (yesButton, obj) => - { - LoadDefaultConfig(setLanguage: false, loadContentPackages: Screen.Selected != GameMain.GameScreen); - CheckBindings(true); - RefreshItemMessages(); - ApplySettings(); - if (Screen.Selected == GameMain.MainMenuScreen) - { - GameMain.MainMenuScreen.ResetSettingsFrame(currentTab); - } - else - { - ResetSettingsFrame(); - CreateSettingsFrame(currentTab); - } - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - }; - - applyButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomRight), - TextManager.Get("ApplySettingsButton")) - { - IgnoreLayoutGroups = true, - Enabled = false - }; - applyButton.OnClicked = ApplyClicked; - -#if DEBUG - /// Debug tab ---------------------------------------------------------------- - var debugTickBoxes = new GUILayoutGroup(new RectTransform(new Vector2(0.28f, 0.15f), tabs[(int)Tab.Debug].RectTransform, Anchor.TopLeft) - { RelativeOffset = new Vector2(0.02f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - void addDebugTickBox(bool initialValue, Action set, string label, string tooltip) - { - var tickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), label, style: "GUITickBox"); - tickBox.Selected = initialValue; - tickBox.ToolTip = tooltip; - tickBox.OnSelected = (tickBox) => - { - set(tickBox.Selected); - UnsavedSettings = true; - return true; - }; - } - - addDebugTickBox( - AutomaticQuickStartEnabled, - (b) => AutomaticQuickStartEnabled = b, - "Automatic quickstart enabled", - "Will the game automatically move on to Quickstart when the game is launched"); - - addDebugTickBox( - TestScreenEnabled, - (b) => TestScreenEnabled = b, - "Test screen enabled", - "Will the game automatically move on to a test screen when the game is launched"); - - addDebugTickBox( - AutomaticCampaignLoadEnabled, - (b) => AutomaticCampaignLoadEnabled = b, - "Automatic campaign load enabled", - "Will the game automatically load the latest campaign save when the game is launched"); - - addDebugTickBox( - EnableSplashScreen, - (b) => EnableSplashScreen = b, - "Splash screen enabled", - "Are the splash screens shown when the game is launched"); - - addDebugTickBox( - VerboseLogging, - (b) => VerboseLogging = b, - "Verbose logging enabled", - "Should verbose logging be used"); - - addDebugTickBox( - TextManagerDebugModeEnabled, - (b) => TextManagerDebugModeEnabled = b, - "TextManager debug mode enabled", - "Does the TextManager return the text tags for debug purposes?"); - - addDebugTickBox( - ModBreakerMode, - (b) => ModBreakerMode = b, - "Mod breaker mode enabled", - "Do horrible things when loading mods to see if it breaks?"); -#endif - - UnsavedSettings = false; // Reset unsaved settings to false once the UI has been created - SelectTab(selectedTab); - } - - private List UpdateResolutionDD(GUIDropDown resolutionDD) - { - var supportedDisplayModes = new List(); - foreach (DisplayMode mode in GraphicsAdapter.DefaultAdapter.SupportedDisplayModes) - { - if (supportedDisplayModes.Any(m => m.Width == mode.Width && m.Height == mode.Height)) { continue; } -#if OSX - // Monogame currently doesn't support retina displays - // so we need to disable resolutions above the viewport size. - - // In a bundled .app you just disable HiDPI in the info.plist - // but that's probably not gonna happen. - if (mode.Width > GameMain.Instance.GraphicsDevice.DisplayMode.Width || mode.Height > GameMain.Instance.GraphicsDevice.DisplayMode.Height) { continue; } -#endif - supportedDisplayModes.Add(mode); - } - supportedDisplayModes.Sort((a, b) => - { - if (a.Width < b.Width) - { - return -1; - } - if (a.Width > b.Width) - { - return 1; - } - if (a.Height < b.Height) - { - return -1; - } - if (a.Height > b.Height) - { - return 1; - } - return 0; - }); - - resolutionDD.ClearChildren(); - - foreach (DisplayMode mode in supportedDisplayModes) - { - if (mode.Width < MinSupportedResolution.X || mode.Height < MinSupportedResolution.Y) { continue; } - resolutionDD.AddItem(mode.Width + "x" + mode.Height, mode); - if (GraphicsWidth == mode.Width && GraphicsHeight == mode.Height) resolutionDD.SelectItem(mode); - } - - if (resolutionDD.SelectedItemData == null) - { - resolutionDD.SelectItem(GraphicsAdapter.DefaultAdapter.SupportedDisplayModes.Last()); - } - - resolutionDD.ListBox.RectTransform.Resize(new Point(resolutionDD.Rect.Width, resolutionDD.Rect.Height * MathHelper.Clamp(supportedDisplayModes.Count, 2, 10))); - - return supportedDisplayModes; - } - - private string TrimAudioDeviceName(string name) - { - if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } - string[] prefixes = { "OpenAL Soft on " }; - foreach (string prefix in prefixes) - { - if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return name.Remove(0, prefix.Length); - } - } - return name; - } - - private Tab currentTab; - private void SelectTab(Tab tab) - { - switch (tab) - { - case Tab.VoiceChat: - if (VoiceSetting != VoiceMode.Disabled) - { - if (GameMain.Client == null && VoipCapture.Instance == null) - { - VoipCapture.Create(GameMain.Config.VoiceCaptureDevice); - } - } - break; - default: - if (GameMain.Client == null) - { - VoipCapture.Instance?.Dispose(); - } - break; - } - for (int i = 0; i < tabs.Length; i++) - { - tabs[i].Visible = (Tab)tabs[i].UserData == tab; - tabButtons[i].Selected = tabs[i].Visible; - } - currentTab = tab; - } - - private void KeyBoxSelected(GUITextBox textBox, Keys key) - { - textBox.Text = ""; - CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, keyMapping)); - } - - private void InventoryKeyBoxSelected(GUITextBox textBox, Keys key) - { - textBox.Text = ""; - CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, inventoryKeyMapping)); - } - - private void ResetControls(bool legacy) - { - // TODO: add a prompt? - SetDefaultBindings(legacy: legacy); - CheckBindings(true); - RefreshItemMessages(); - ApplySettings(); - if (Screen.Selected == GameMain.MainMenuScreen) - { - GameMain.MainMenuScreen.ResetSettingsFrame(Tab.Controls); - } - else - { - ResetSettingsFrame(); - CreateSettingsFrame(Tab.Controls); - } - } - - private bool SelectResolution(GUIComponent selected, object userData) - { - DisplayMode mode = selected.UserData as DisplayMode; - if (mode == null) return false; - - if (GraphicsWidth == mode.Width && GraphicsHeight == mode.Height) return false; - - GraphicsWidth = mode.Width; - GraphicsHeight = mode.Height; - GameMain.Instance.ApplyGraphicsSettings(); - UnsavedSettings = true; - - return true; - } - - private bool CanHotswapPackages(bool core) - { - return GameMain.Client == null && - (ContentPackage.IngameModSwap || - Screen.Selected != GameMain.GameScreen && - Screen.Selected != GameMain.SubEditorScreen) && - (!core || - (Screen.Selected != GameMain.CharacterEditorScreen && - Screen.Selected != GameMain.ParticleEditorScreen)); - } - - private bool SelectCorePackage(GUIComponent component, object userData) - { - if (!(userData is ContentPackage contentPackage) || GameMain.Client != null) { return false; } - - SelectCorePackage(contentPackage); - - UnsavedSettings = true; - return true; - } - - private void OnContentPackagesRearranged(GUIListBox listBox, object userData) - { - if (GameMain.Client != null) { return; } - - if (userData is ContentPackage contentPackage) - { - if (!EnabledRegularPackages.Contains(contentPackage)) { return; } - } - - ContentPackage.SortContentPackages(cp => listBox.Content.GetChildIndex(listBox.Content.GetChildByUserData(cp)), true, this); - - UnsavedSettings = true; - } - - private bool SelectContentPackage(GUITickBox tickBox) - { - if (GameMain.Client != null) { return false; } - - var contentPackage = tickBox.UserData as ContentPackage; - - if (tickBox.Selected) - { - EnableRegularPackage(contentPackage); - } - else - { - DisableRegularPackage(contentPackage); - } - - ContentPackage.SortContentPackages(cp => contentPackageList.Content.GetChildIndex(contentPackageList.Content.GetChildByUserData(cp)), false, this); - - UnsavedSettings = true; - return true; - } - - private IEnumerable WaitForKeyPress(GUITextBox keyBox, KeyOrMouse[] keyArray) - { - yield return CoroutineStatus.Running; - - while (PlayerInput.PrimaryMouseButtonHeld() || PlayerInput.PrimaryMouseButtonClicked()) - { - //wait for the mouse to be released, so that we don't interpret clicking on the textbox as the keybinding - yield return CoroutineStatus.Running; - } - while (keyBox.Selected && PlayerInput.GetKeyboardState.GetPressedKeys().Length == 0 && - !PlayerInput.LeftButtonClicked() && !PlayerInput.RightButtonClicked() && !PlayerInput.MidButtonClicked() && - !PlayerInput.Mouse4ButtonClicked() && !PlayerInput.Mouse5ButtonClicked() && !PlayerInput.MouseWheelUpClicked() && !PlayerInput.MouseWheelDownClicked()) - { - if (Screen.Selected != GameMain.MainMenuScreen && !GUI.SettingsMenuOpen) yield return CoroutineStatus.Success; - - yield return CoroutineStatus.Running; - } - - UnsavedSettings = true; - - int keyIndex = (int)keyBox.UserData; - - if (PlayerInput.LeftButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.LeftMouse); - } - else if (PlayerInput.RightButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.RightMouse); - } - else if (PlayerInput.MidButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MiddleMouse); - } - else if (PlayerInput.Mouse4ButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton4); - } - else if (PlayerInput.Mouse5ButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton5); - } - else if (PlayerInput.MouseWheelUpClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelUp); - } - else if (PlayerInput.MouseWheelDownClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelDown); - } - else if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0) - { - Keys key = PlayerInput.GetKeyboardState.GetPressedKeys()[0]; - keyArray[keyIndex] = new KeyOrMouse(key); - } - else - { - yield return CoroutineStatus.Success; - } - - keyBox.Text = keyArray[keyIndex].Name; - keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, keyBox.Rect.Width); - - keyBox.Deselect(); - RefreshItemMessages(); - - yield return CoroutineStatus.Success; - } - - private void RefreshItemMessages() - { - foreach (Item item in Item.ItemList) - { - foreach (Items.Components.ItemComponent ic in item.Components) - { - ic.ParseMsg(); - } - } - CharacterHUD.ShouldRecreateHudTexts = true; - } - - private void ApplySettings() - { - SaveNewPlayerConfig(); - - SettingsFrame.Flash(GUI.Style.Green); - - if (textScaleDirty || GameMain.WindowMode != GameMain.Config.WindowMode || GameMain.Config.GraphicsWidth != GameMain.GraphicsWidth || GameMain.Config.GraphicsHeight != GameMain.GraphicsHeight) - { - GameMain.Instance.ApplyGraphicsSettings(); - textScaleDirty = false; - } - } - - private bool ApplyClicked(GUIButton button, object userData) - { - ApplySettings(); - if (Screen.Selected != GameMain.MainMenuScreen) { GUI.SettingsMenuOpen = false; } - WarnIfContentPackageSelectionDirty(); - return true; - } - - public void WarnIfContentPackageSelectionDirty() - { - if (ContentPackageSelectionDirtyNotification) - { - new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredContentPackage", fallBackTag: "RestartRequiredGeneric")); - ContentPackageSelectionDirtyNotification = false; - } - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 88d4d7135..e19ace1a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -109,7 +109,7 @@ namespace Barotrauma indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; indicatorGroup.ChildAnchor = Anchor.TopCenter; - indicatorSpriteSize = GUI.Style.GetComponentStyle("EquipmentIndicatorDivingSuit").GetDefaultSprite().size; + indicatorSpriteSize = GUIStyle.GetComponentStyle("EquipmentIndicatorDivingSuit").GetDefaultSprite().size; indicators[0] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorDivingSuit"); indicators[1] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorID"); @@ -522,7 +522,7 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam, bool isSubInventory = false) { - if (!AccessibleWhenAlive && !character.IsDead) + if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { syncItemsDelay = Math.Max(syncItemsDelay - deltaTime, 0.0f); return; @@ -814,21 +814,21 @@ namespace Barotrauma if (conditionPercentage != -1) { - indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUI.Style.EquipmentIndicatorRunningOut, GUI.Style.EquipmentIndicatorEquipped); + indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUIStyle.EquipmentIndicatorRunningOut, GUIStyle.EquipmentIndicatorEquipped); } else { - indicators[i].Color = GUI.Style.EquipmentIndicatorRunningOut; + indicators[i].Color = GUIStyle.EquipmentIndicatorRunningOut; } } else { - indicators[i].Color = GUI.Style.EquipmentIndicatorEquipped; + indicators[i].Color = GUIStyle.EquipmentIndicatorEquipped; } } else { - indicators[i].Color = GUI.Style.EquipmentIndicatorNotEquipped; + indicators[i].Color = GUIStyle.EquipmentIndicatorNotEquipped; } } } @@ -1007,7 +1007,7 @@ namespace Barotrauma var slot = invSlots[i]; if (item.ParentInventory.GetItemAt(i) == item) { - slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); SoundPlayer.PlayUISound(GUISoundType.PickItem); break; } @@ -1033,7 +1033,7 @@ namespace Barotrauma { if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "equipconfirmation")) { return; } var equipConfirmation = new GUIMessageBox(string.Empty, TextManager.Get(item.Prefab.EquipConfirmationText), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }) + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }) { UserData = "equipconfirmation" }; @@ -1138,7 +1138,7 @@ namespace Barotrauma success = true; for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(heldItem)) { visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); } + if (slots[j].Contains(heldItem)) { visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } break; } @@ -1150,7 +1150,7 @@ namespace Barotrauma { for (int i = 0; i < capacity; i++) { - if (slots[i].Contains(item)) { visualSlots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); } + if (slots[i].Contains(item)) { visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } } @@ -1163,7 +1163,7 @@ namespace Barotrauma public void DrawOwn(SpriteBatch spriteBatch) { - if (!AccessibleWhenAlive && !character.IsDead) { return; } + if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { return; } if (capacity == 0) { return; } if (visualSlots == null) { CreateSlots(); } if (GameMain.GraphicsWidth != screenResolution.X || @@ -1182,7 +1182,7 @@ namespace Barotrauma CalculateBackgroundFrame(); GUI.DrawRectangle(spriteBatch, BackgroundFrame, Color.Black * 0.8f, true); GUI.DrawString(spriteBatch, - new Vector2((int)(BackgroundFrame.Center.X - GUI.Font.MeasureString(character.Name).X / 2), (int)BackgroundFrame.Y + 5), + new Vector2((int)(BackgroundFrame.Center.X - GUIStyle.Font.MeasureString(character.Name).X / 2), (int)BackgroundFrame.Y + 5), character.Name, Color.White * 0.9f); } @@ -1218,7 +1218,7 @@ namespace Barotrauma if (LimbSlotIcons.ContainsKey(SlotTypes[i])) { var icon = LimbSlotIcons[SlotTypes[i]]; - icon.Draw(spriteBatch, visualSlots[i].Rect.Center.ToVector2() + visualSlots[i].DrawOffset, GUI.Style.EquipmentSlotIconColor, origin: icon.size / 2, scale: visualSlots[i].Rect.Width / icon.size.X); + icon.Draw(spriteBatch, visualSlots[i].Rect.Center.ToVector2() + visualSlots[i].DrawOffset, GUIStyle.EquipmentSlotIconColor, origin: icon.size / 2, scale: visualSlots[i].Rect.Width / icon.size.X); } continue; } @@ -1292,14 +1292,14 @@ namespace Barotrauma if (Locked) { GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true); - var lockIcon = GUI.Style.GetComponentStyle("LockIcon")?.GetDefaultSprite(); + var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite(); lockIcon?.Draw(spriteBatch, inventoryArea.Center.ToVector2(), scale: Math.Min(inventoryArea.Height / lockIcon.size.Y * 0.7f, 1.0f)); if (inventoryArea.Contains(PlayerInput.MousePosition)) { GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height))); } } - else if (highlightedQuickUseSlot != null && !string.IsNullOrEmpty(highlightedQuickUseSlot.QuickUseButtonToolTip)) + else if (highlightedQuickUseSlot != null && !highlightedQuickUseSlot.QuickUseButtonToolTip.IsNullOrEmpty()) { GUIComponent.DrawToolTip(spriteBatch, highlightedQuickUseSlot.QuickUseButtonToolTip, highlightedQuickUseSlot.EquipButtonRect); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 1b8a90f60..5ed620e91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components //openState when the vertices of the convex hull were last calculated private float lastConvexHullState; - [Serialize("1,1", false, description: "The scale of the shadow-casting area of the door (relative to the actual size of the door).")] + [Serialize("1,1", IsPropertySaveable.No, description: "The scale of the shadow-casting area of the door (relative to the actual size of the door).")] public Vector2 ShadowScale { get; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs index 97c661961..a7229fbf9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs @@ -17,12 +17,12 @@ namespace Barotrauma.Items.Components case AreaShape.Rectangle: { RectangleF rect = GetAreaRectangle(SpawnAreaBounds, SpawnAreaOffset, draw: true); - GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Red, isFilled: false, 0f, 4f); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUIStyle.Red, isFilled: false, 0f, 4f); if (MaximumAmountRangePadding > 0f) { rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding); - GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Red, isFilled: false, 0f, 2f); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUIStyle.Red, isFilled: false, 0f, 2f); } break; } @@ -30,11 +30,11 @@ namespace Barotrauma.Items.Components Vector2 center = item.WorldPosition; center += SpawnAreaOffset; center.Y = -center.Y; - spriteBatch.DrawCircle(center, SpawnAreaRadius, 32, GUI.Style.Red, thickness: 4f); + spriteBatch.DrawCircle(center, SpawnAreaRadius, 32, GUIStyle.Red, thickness: 4f); if (MaximumAmountRangePadding > 0f) { - spriteBatch.DrawCircle(center, SpawnAreaRadius + MaximumAmountRangePadding, 32, GUI.Style.Red, thickness: 2f); + spriteBatch.DrawCircle(center, SpawnAreaRadius + MaximumAmountRangePadding, 32, GUIStyle.Red, thickness: 2f); } break; } @@ -46,14 +46,14 @@ namespace Barotrauma.Items.Components case AreaShape.Rectangle: { RectangleF rect = GetAreaRectangle(CrewAreaBounds, CrewAreaOffset, draw: true); - GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Green, isFilled: false, 0f, 4f); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUIStyle.Green, isFilled: false, 0f, 4f); break; } case AreaShape.Circle: Vector2 center = item.WorldPosition; center += CrewAreaOffset; center.Y = -center.Y; - spriteBatch.DrawCircle(center, CrewAreaRadius, 32, GUI.Style.Green); + spriteBatch.DrawCircle(center, CrewAreaRadius, 32, GUIStyle.Green); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 174fba2b0..33dafbe91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -6,17 +6,17 @@ namespace Barotrauma.Items.Components { partial class GeneticMaterial : ItemComponent { - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float TooltipValueMin { get; set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float TooltipValueMax { get; set; } - public override void AddTooltipInfo(ref string name, ref string description) + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { - if (!string.IsNullOrEmpty(materialName) && item.ContainedItems.Count() > 0) + if (!materialName.IsNullOrEmpty() && item.ContainedItems.Count() > 0) { - string mergedMaterialName = materialName; + LocalizedString mergedMaterialName = materialName; foreach (Item containedItem in item.ContainedItems) { var containedMaterial = containedItem.GetComponent(); @@ -31,30 +31,30 @@ namespace Barotrauma.Items.Components name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name); } - if (TextManager.ContainsTag("entitydescription." + Item.prefab.Identifier)) + if (TextManager.ContainsTag("entitydescription." + Item.Prefab.Identifier)) { int value = (int)MathHelper.Lerp(TooltipValueMin, TooltipValueMax, item.ConditionPercentage / 100.0f); - description = TextManager.GetWithVariable("entitydescription." + Item.prefab.Identifier, "[value]", value.ToString()); + description = TextManager.GetWithVariable("entitydescription." + Item.Prefab.Identifier, "[value]", value.ToString()); } foreach (Item containedItem in item.ContainedItems) { var containedGeneticMaterial = containedItem.GetComponent(); if (containedGeneticMaterial == null) { continue; } - string _ = string.Empty; - string containedDescription = containedItem.Description; + LocalizedString _ = string.Empty; + LocalizedString containedDescription = containedItem.Description; containedGeneticMaterial.AddTooltipInfo(ref _, ref containedDescription); - if (!string.IsNullOrEmpty(containedDescription)) + if (!containedDescription.IsNullOrEmpty()) { description += '\n' + containedDescription; } } } - public void ModifyDeconstructInfo(Deconstructor deconstructor, ref string buttonText, ref string infoText) + public void ModifyDeconstructInfo(Deconstructor deconstructor, ref LocalizedString buttonText, ref LocalizedString infoText) { if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2) { - if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.prefab == item.prefab)) + if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.Prefab == item.Prefab)) { buttonText = TextManager.Get("researchstation.combine"); infoText = TextManager.Get("researchstation.combine.infotext"); @@ -74,12 +74,12 @@ namespace Barotrauma.Items.Components if (Tainted) { uint selectedTaintedEffectId = msg.ReadUInt32(); - selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedTaintedEffectId); + selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.UintIdentifier == selectedTaintedEffectId); } else { uint selectedEffectId = msg.ReadUInt32(); - selectedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedEffectId); + selectedEffect = AfflictionPrefab.Prefabs.Find(a => a.UintIdentifier == selectedEffectId); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index f5a2cda6e..67ce2148e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -10,15 +11,15 @@ namespace Barotrauma.Items.Components { internal class VineSprite { - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Rectangle SourceRect { get; private set; } - [Serialize("0.5,0.5", false)] + [Serialize("0.5,0.5", IsPropertySaveable.No)] public Vector2 Origin { get; private set; } public Vector2 AbsoluteOrigin; - public VineSprite(XElement element) + public VineSprite(ContentXElement element) { SerializableProperty.DeserializeProperties(this, element); AbsoluteOrigin = new Vector2(SourceRect.Width * Origin.X, SourceRect.Height * Origin.Y); @@ -109,28 +110,27 @@ namespace Barotrauma.Items.Components } } - partial void LoadVines(XElement element) + partial void LoadVines(ContentXElement element) { - string? vineAtlasPath = element.GetAttributeString("vineatlas", null); - string? decayAtlasPath = element.GetAttributeString("decayatlas", null); + ContentPath vineAtlasPath = element.GetAttributeContentPath("vineatlas") ?? ContentPath.Empty; + ContentPath decayAtlasPath = element.GetAttributeContentPath("decayatlas") ?? ContentPath.Empty; - if (vineAtlasPath != null) + if (!vineAtlasPath.IsNullOrEmpty()) { - VineAtlas = new Sprite(vineAtlasPath, Rectangle.Empty); + VineAtlas = new Sprite(vineAtlasPath.Value, Rectangle.Empty); } - if (decayAtlasPath != null) + if (!decayAtlasPath.IsNullOrEmpty()) { - DecayAtlas = new Sprite(decayAtlasPath, Rectangle.Empty); + DecayAtlas = new Sprite(decayAtlasPath.Value, Rectangle.Empty); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "vinesprite": - var tileType = subElement.GetAttributeString("type", null); - VineTileType type = Enum.Parse(tileType); + VineTileType type = subElement.GetAttributeEnum("type", VineTileType.Stem); VineSprites.Add(type, new VineSprite(subElement)); break; case "flowersprite": @@ -145,11 +145,11 @@ namespace Barotrauma.Items.Components leafVariants = LeafSprites.Count; } - foreach (VineTileType type in Enum.GetValues(typeof(VineTileType))) + foreach (VineTileType type in Enum.GetValues(typeof(VineTileType)).Cast()) { if (!VineSprites.ContainsKey(type)) { - DebugConsole.ThrowError($"Vine sprite missing from {item.prefab.Identifier}: {type}"); + DebugConsole.ThrowError($"Vine sprite missing from {item.Prefab.Identifier}: {type}"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 4ee0d2b1f..47a8e6089 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components item.SpriteColor * 0.5f, 0.0f, item.Scale, SpriteEffects.None, 0.0f); - GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUI.Style.Red, thickness: 3); + GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUIStyle.Red, thickness: 3); } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index ebd3e27a8..b68703a72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -21,107 +21,44 @@ namespace Barotrauma.Items.Components public Color FacialHairColor; public Color SkinColor; - public void ExtractJobPrefab(string[] tags) + public void ExtractJobPrefab(IReadOnlyDictionary tags) { - string jobIdTag = tags.FirstOrDefault(s => s.StartsWith("jobid:")); - - if (jobIdTag != null && jobIdTag.Length > 6) + if (!tags.TryGetValue("jobid".ToIdentifier(), out string jobId)) { return; } + + if (!jobId.IsNullOrEmpty()) { - string jobId = jobIdTag.Substring(6); - if (jobId != string.Empty) - { - JobPrefab = JobPrefab.Get(jobId); - } + JobPrefab = JobPrefab.Get(jobId); } } - public void ExtractAppearance(CharacterInfo characterInfo, string[] tags) + public void ExtractAppearance(CharacterInfo characterInfo, IdCard idCard) { - Gender disguisedGender = Gender.None; - Race disguisedRace = Race.None; - int disguisedHeadSpriteId = -1; - int disguisedHairIndex = -1; - int disguisedBeardIndex = -1; - int disguisedMoustacheIndex = -1; - int disguisedFaceAttachmentIndex = -1; - Color hairColor = Color.Black; - Color facialHairColor = Color.Black; - Color skinColor = Color.Black; + int disguisedHairIndex = idCard.OwnerHairIndex; + int disguisedBeardIndex = idCard.OwnerBeardIndex; + int disguisedMoustacheIndex = idCard.OwnerMoustacheIndex; + int disguisedFaceAttachmentIndex = idCard.OwnerFaceAttachmentIndex; + Color hairColor = idCard.OwnerHairColor; + Color facialHairColor = idCard.OwnerFacialHairColor; + Color skinColor = idCard.OwnerSkinColor; + var tags = idCard.OwnerTagSet; - foreach (string tag in tags) - { - string[] s = tag.Split(':'); - - switch (s[0].ToLowerInvariant()) - { - case "haircolor": - hairColor = XMLExtensions.ParseColor(s[1]); - break; - - case "facialhaircolor": - facialHairColor = XMLExtensions.ParseColor(s[1]); - break; - - case "skincolor": - skinColor = XMLExtensions.ParseColor(s[1]); - break; - - case "gender": - Enum.TryParse(s[1], ignoreCase: true, out disguisedGender); - break; - - case "race": - Enum.TryParse(s[1], ignoreCase: true, out disguisedRace); - break; - - case "headspriteid": - int.TryParse(s[1], NumberStyles.Any, CultureInfo.InvariantCulture, out disguisedHeadSpriteId); - break; - - case "hairindex": - disguisedHairIndex = int.Parse(s[1]); - break; - - case "beardindex": - disguisedBeardIndex = int.Parse(s[1]); - break; - - case "moustacheindex": - disguisedMoustacheIndex = int.Parse(s[1]); - break; - - case "faceattachmentindex": - disguisedFaceAttachmentIndex = int.Parse(s[1]); - break; - - case "sheetindex": - string[] vectorValues = s[1].Split(";"); - SheetIndex = new Vector2(float.Parse(vectorValues[0]), float.Parse(vectorValues[1])); - break; - } - } - - if ((characterInfo.HasGenders && disguisedGender == Gender.None) - || (characterInfo.HasRaces && disguisedRace == Race.None) - || disguisedHeadSpriteId <= 0) + if ((characterInfo.HasSpecifierTags && !tags.Any())) { Portrait = null; Attachments = null; return; } - foreach (XElement limbElement in characterInfo.Ragdoll.MainElement.Elements()) + foreach (ContentXElement limbElement in characterInfo.Ragdoll.MainElement.Elements()) { if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } - XElement spriteElement = limbElement.Element("sprite"); + ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; - spritePath = spritePath.Replace("[GENDER]", disguisedGender.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[RACE]", disguisedRace.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", disguisedHeadSpriteId.ToString()); + spritePath = characterInfo.ReplaceVars(spritePath); string fileName = Path.GetFileNameWithoutExtension(spritePath); @@ -144,13 +81,11 @@ namespace Barotrauma.Items.Components if (characterInfo.Wearables != null) { - float baldnessChance = disguisedGender == Gender.Female ? 0.05f : 0.2f; + float baldnessChance = 0.1f; - List createElementList(WearableType wearableType, float emptyCommonness = 1.0f) + List createElementList(WearableType wearableType, float emptyCommonness = 1.0f) => CharacterInfo.AddEmpty( - characterInfo.FilterByTypeAndHeadID( - characterInfo.FilterElementsByGenderAndRace(characterInfo.Wearables, disguisedGender, disguisedRace), - wearableType, disguisedHeadSpriteId), + characterInfo.FilterElements(characterInfo.Wearables, tags, wearableType), wearableType, emptyCommonness); var disguisedHairs = createElementList(WearableType.Hair, baldnessChance); @@ -158,7 +93,7 @@ namespace Barotrauma.Items.Components var disguisedMoustaches = createElementList(WearableType.Moustache); var disguisedFaceAttachments = createElementList(WearableType.FaceAttachment); - XElement getElementFromList(List list, int index) + ContentXElement getElementFromList(List list, int index) => CharacterInfo.IsValidIndex(index, list) ? list[index] : characterInfo.GetRandomElement(list); @@ -170,9 +105,9 @@ namespace Barotrauma.Items.Components Attachments = new List(); - void loadAttachments(List attachments, XElement element, WearableType wearableType) + void loadAttachments(List attachments, ContentXElement element, WearableType wearableType) { - foreach (var s in element?.Elements("sprite") ?? Enumerable.Empty()) + foreach (var s in element?.GetChildElements("sprite") ?? Enumerable.Empty()) { attachments.Add(new WearableSprite(s, wearableType)); } @@ -185,7 +120,7 @@ namespace Barotrauma.Items.Components loadAttachments(Attachments, characterInfo.OmitJobInPortraitClothing - ? JobPrefab.NoJobElement?.Element("PortraitClothing") + ? JobPrefab.NoJobElement?.GetChildElement("PortraitClothing") : JobPrefab?.ClothingElement, WearableType.JobIndicator); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 0a3f1ddb7..39982b6cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -26,29 +26,28 @@ namespace Barotrauma.Items.Components private readonly List particleEmitters = new List(); private readonly List particleEmitterCharges = new List(); - [Serialize(1.0f, false, description: "The scale of the crosshair sprite (if there is one).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "The scale of the crosshair sprite (if there is one).")] public float CrossHairScale { get; private set; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { + string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) { case "crosshair": { - string texturePath = subElement.GetAttributeString("texture", ""); - crosshairSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairSprite = new Sprite(subElement, path: textureDir); } break; case "crosshairpointer": { - string texturePath = subElement.GetAttributeString("texture", ""); - crosshairPointerSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairPointerSprite = new Sprite(subElement, path: textureDir); } break; case "particleemitter": @@ -58,7 +57,7 @@ namespace Barotrauma.Items.Components particleEmitterCharges.Add(new ParticleEmitter(subElement)); break; case "chargesound": - chargeSound = Submarine.LoadRoundSound(subElement, false); + chargeSound = RoundSound.Load(subElement, false); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index b3057fb3e..f97cdc3be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -30,11 +30,11 @@ namespace Barotrauma.Items.Components private Color color; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { currentCrossHairPointerScale = element.GetAttributeFloat("crosshairscale", 0.1f); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -243,7 +243,7 @@ namespace Barotrauma.Items.Components if (liquidItem == null) { return; } bool isCleaning = false; - liquidColors.TryGetValue(liquidItem.prefab.Identifier, out color); + liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color); // Ethanol or other cleaning solvent if (color.A == 0) { isCleaning = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 49d0725d2..595945e37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Items.Components public readonly RoundSound RoundSound; public readonly ActionType Type; - public string VolumeProperty; + public Identifier VolumeProperty; public float VolumeMultiplier { @@ -145,7 +145,7 @@ namespace Barotrauma.Items.Components public GUIFrame GuiFrame { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool AllowUIOverlap { get; @@ -153,21 +153,21 @@ namespace Barotrauma.Items.Components } private ItemComponent linkToUIComponent; - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string LinkUIToComponent { get; set; } - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int HudPriority { get; private set; } - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int HudLayer { get; @@ -457,14 +457,14 @@ namespace Barotrauma.Items.Components { } - private bool LoadElemProjSpecific(XElement subElement) + private bool LoadElemProjSpecific(ContentXElement subElement) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "guiframe": if (subElement.Attribute("rect") != null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - GUIFrame defined as rect, use RectTransform instead."); + DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead."); break; } GuiFrameSource = subElement; @@ -475,21 +475,18 @@ namespace Barotrauma.Items.Components break; case "itemsound": case "sound": - string filePath = subElement.GetAttributeString("file", ""); + //TODO: this validation stuff should probably go somewhere else + string filePath = subElement.GetAttributeStringUnrestricted("file", ""); - if (filePath == "") filePath = subElement.GetAttributeString("sound", ""); + if (filePath.IsNullOrEmpty()) { filePath = subElement.GetAttributeStringUnrestricted("sound", ""); } - if (filePath == "") + if (filePath.IsNullOrEmpty()) { - DebugConsole.ThrowError("Error when instantiating item \"" + item.Name + "\" - sound with no file path set"); + DebugConsole.ThrowError( + $"Error when instantiating item \"{item.Name}\" - sound with no file path set"); break; } - if (!filePath.Contains("/") && !filePath.Contains("\\") && !filePath.Contains(Path.DirectorySeparatorChar)) - { - filePath = Path.Combine(Path.GetDirectoryName(item.Prefab.FilePath), filePath); - } - ActionType type; try { @@ -501,11 +498,11 @@ namespace Barotrauma.Items.Components break; } - RoundSound sound = Submarine.LoadRoundSound(subElement); + RoundSound sound = RoundSound.Load(subElement); if (sound == null) { break; } ItemSound itemSound = new ItemSound(sound, type, subElement.GetAttributeBool("loop", false)) { - VolumeProperty = subElement.GetAttributeString("volumeproperty", "").ToLowerInvariant() + VolumeProperty = subElement.GetAttributeIdentifier("volumeproperty", "") }; if (soundSelectionModes == null) soundSelectionModes = new Dictionary(); @@ -621,6 +618,7 @@ namespace Barotrauma.Items.Components } OnResolutionChanged(); } - public virtual void AddTooltipInfo(ref string name, ref string description) { } + + public virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 2154f25ea..597c90daa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -49,25 +49,25 @@ namespace Barotrauma.Items.Components /// /// Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used. /// - [Serialize(-1.0f, false, description: "Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used.")] + [Serialize(-1.0f, IsPropertySaveable.No, description: "Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used.")] public float ContainedSpriteDepth { get; set; } - [Serialize(null, false, description: "An optional text displayed above the item's inventory.")] + [Serialize(null, IsPropertySaveable.No, description: "An optional text displayed above the item's inventory.")] public string UILabel { get; set; } public GUIComponentStyle IndicatorStyle { get; set; } - [Serialize(null, false)] + [Serialize(null, IsPropertySaveable.No)] public string ContainedStateIndicatorStyle { get; set; } - [Serialize(-1, false, description: "Can be used to make the contained state indicator display the condition of the item in a specific slot even when the container's capacity is more than 1.")] + [Serialize(-1, IsPropertySaveable.No, description: "Can be used to make the contained state indicator display the condition of the item in a specific slot even when the container's capacity is more than 1.")] public int ContainedStateIndicatorSlot { get; set; } - [Serialize(true, false, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ - "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] + [Serialize(true, IsPropertySaveable.No, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ + "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] public bool ShowContainedStateIndicator { get; set; } - [Serialize(false, false, description: "If enabled, the condition of this item is displayed in the indicator that would normally show the state of the contained items." + + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the condition of this item is displayed in the indicator that would normally show the state of the contained items." + " May be useful for items such as ammo boxes and magazines that spawn projectiles as needed," + " and use the condition to determine how many projectiles can be spawned in total.")] public bool ShowConditionInContainedStateIndicator @@ -76,13 +76,13 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")] + [Serialize(false, IsPropertySaveable.No, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")] public bool ShowTotalStackCapacityInContainedStateIndicator { get; set; } - [Serialize(false, false, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] public bool KeepOpenWhenEquipped { get; set; } - [Serialize(false, false, description: "Can the inventory of this item be moved around on the screen by the player.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the inventory of this item be moved around on the screen by the player.")] public bool MovableFrame { get; set; } public Vector2 DrawSize @@ -91,10 +91,10 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { slotIcons = new Sprite[capacity]; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -132,12 +132,12 @@ namespace Barotrauma.Items.Components //if neither a style or a custom sprite is defined, use default style if (ContainedStateIndicator == null) { - IndicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator.Default"); + IndicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator.Default"); } } else { - IndicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator." + ContainedStateIndicatorStyle); + IndicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator." + ContainedStateIndicatorStyle); if (ContainedStateIndicator != null || ContainedStateIndicatorEmpty != null) { DebugConsole.AddWarning($"Item \"{item.Name}\" defines both a contained state indicator style and a custom indicator sprite. Will use the custom sprite..."); @@ -165,7 +165,7 @@ namespace Barotrauma.Items.Components CreateGUI(); } - containedSpriteDepths = element.GetAttributeFloatArray("containedspritedepths", new float[0]); + containedSpriteDepths = element.GetAttributeFloatArray("containedspritedepths", Array.Empty()); } protected override void CreateGUI() @@ -176,12 +176,12 @@ namespace Barotrauma.Items.Components CanBeFocused = false }; - string labelText = GetUILabel(); + LocalizedString labelText = GetUILabel(); GUITextBlock label = null; - if (!string.IsNullOrEmpty(labelText)) + if (!labelText.IsNullOrEmpty()) { label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopCenter), - labelText, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); } float minInventoryAreaSize = 0.5f; @@ -212,12 +212,12 @@ namespace Barotrauma.Items.Components Inventory.RectTransform = guiCustomComponent.RectTransform; } - public string GetUILabel() + public LocalizedString GetUILabel() { if (UILabel == string.Empty) { return string.Empty; } if (UILabel != null) { - return TextManager.Get("UILabel." + UILabel, returnNull: true) ?? TextManager.Get(UILabel); + return TextManager.Get("UILabel." + UILabel).Fallback(TextManager.Get(UILabel)); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 8aba265ce..076f6621b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components private Vector4 padding; - [Serialize("0,0,0,0", true, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] public Vector4 Padding { get { return padding; } @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components } private string text; - [Serialize("", true, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)] + [Serialize("", IsPropertySaveable.Yes, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)] public string Text { get { return text; } @@ -60,7 +60,7 @@ namespace Barotrauma.Items.Components private bool ignoreLocalization; - [Editable, Serialize(false, true, "Whether or not to skip localization and always display the raw value.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, "Whether or not to skip localization and always display the raw value.")] public bool IgnoreLocalization { get => ignoreLocalization; @@ -71,13 +71,13 @@ namespace Barotrauma.Items.Components } } - public string DisplayText + public LocalizedString DisplayText { get; private set; } - [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label (R,G,B,A).", alwaysUseInstanceValues: true)] + [Editable, Serialize("0,0,0,255", IsPropertySaveable.Yes, description: "The color of the text displayed on the label (R,G,B,A).", alwaysUseInstanceValues: true)] public Color TextColor { get { return textColor; } @@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, 10.0f), Serialize(1.0f, true, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] + [Editable(0.0f, 10.0f), Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] public float TextScale { get { return textBlock == null ? 1.0f : textBlock.TextScale; } @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components } private bool scrollable; - [Serialize(false, true, description: "Should the text scroll horizontally across the item if it's too long to be displayed all at once.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the text scroll horizontally across the item if it's too long to be displayed all at once.")] public bool Scrollable { get { return scrollable; } @@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(20.0f, true, description: "How fast the text scrolls across the item (only valid if Scrollable is set to true).")] + [Serialize(20.0f, IsPropertySaveable.Yes, description: "How fast the text scrolls across the item (only valid if Scrollable is set to true).")] public float ScrollSpeed { get; @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components } } - public ItemLabel(Item item, XElement element) + public ItemLabel(Item item, ContentXElement element) : base(item, element) { } @@ -148,13 +148,13 @@ namespace Barotrauma.Items.Components //(so the text can scroll entirely out of view before we reset it back to start) needsScrolling = true; float spaceWidth = textBlock.Font.MeasureChar(' ').X; - scrollingText = new string(' ', (int)Math.Ceiling(textAreaWidth / spaceWidth)) + DisplayText; + scrollingText = new string(' ', (int)Math.Ceiling(textAreaWidth / spaceWidth)) + DisplayText.Value; } else { //whole text can fit in the textblock, no need to scroll needsScrolling = false; - scrollingText = DisplayText; + scrollingText = DisplayText.Value; scrollPadding = 0; scrollAmount = 0.0f; scrollIndex = 0; @@ -176,7 +176,7 @@ namespace Barotrauma.Items.Components private void SetDisplayText(string value) { - DisplayText = IgnoreLocalization ? value : TextManager.Get(value, returnNull: true) ?? value; + DisplayText = IgnoreLocalization ? value : TextManager.Get(value).Fallback(value); TextBlock.Text = DisplayText; if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) { @@ -189,7 +189,7 @@ namespace Barotrauma.Items.Components private void RecreateTextBlock() { textBlock = new GUITextBlock(new RectTransform(item.Rect.Size), "", - textColor: textColor, font: GUI.UnscaledSmallFont, textAlignment: scrollable ? Alignment.CenterLeft : Alignment.Center, wrap: !scrollable, style: null) + textColor: textColor, font: GUIStyle.UnscaledSmallFont, textAlignment: scrollable ? Alignment.CenterLeft : Alignment.Center, wrap: !scrollable, style: null) { TextDepth = item.SpriteDepth - 0.00001f, RoundToNearestPixel = false, @@ -261,6 +261,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { + if (item.ParentInventory != null) { return; } if (editing) { if (!MathUtils.NearlyEqual(prevScale, item.Scale) || prevRect != item.Rect) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs index 52f925579..8911109d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components depth: BackgroundSpriteDepth); } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { var backgroundSpriteElement = element.GetChildElement("backgroundsprite"); if (backgroundSpriteElement != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index c2a3d7957..448c56945 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -44,17 +44,22 @@ namespace Barotrauma.Items.Components { if (ParentBody != null) { - Light.Position = ParentBody.Position; + Light.ParentBody = ParentBody; } else if (turret != null) { Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y); } + else if (item.body != null) + { + Light.ParentBody = item.body; + } else { - Light.Position = item.Position; + Light.Position = item.DrawPosition; + if (item.Submarine != null) { Light.Position -= item.Submarine.DrawPosition; } } - PhysicsBody body = ParentBody ?? item.body; + PhysicsBody body = Light.ParentBody; if (body != null) { Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; @@ -74,7 +79,9 @@ namespace Barotrauma.Items.Components Vector2 origin = Light.LightSprite.Origin; if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; } - Light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); + + Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition; + Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index ae6905a0b..67cc5fe8b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -21,13 +21,13 @@ namespace Barotrauma.Items.Components private GUITextBlock infoArea; - [Serialize("DeconstructorDeconstruct", true)] + [Serialize("DeconstructorDeconstruct", IsPropertySaveable.Yes)] public string ActivateButtonText { get; set; } - - [Serialize("", true)] + + [Serialize("", IsPropertySaveable.Yes)] public string InfoText { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float InfoAreaWidth { get; set; } partial void InitProjSpecific(XElement element) @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.08f }; - new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform), item.Name, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform), item.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleHorizontal = true @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.05f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("deconstructor.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("deconstructor.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, inputLabelArea.RectTransform), style: "HorizontalLine"); @@ -80,9 +80,9 @@ namespace Barotrauma.Items.Components TextBlock = { AutoScaleHorizontal = true }, OnClicked = ToggleActive }; - inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("DeconstructorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) - { + inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), + TextManager.Get("DeconstructorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) + { HoverColor = Color.Black, IgnoreLayoutGroups = true, Visible = false, @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.05f }; - var outputLabel = new GUITextBlock(new RectTransform(new Vector2(0f, 1.0f), outputLabelArea.RectTransform), TextManager.Get("uilabel.output"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var outputLabel = new GUITextBlock(new RectTransform(new Vector2(0f, 1.0f), outputLabelArea.RectTransform), TextManager.Get("uilabel.output"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; outputLabel.RectTransform.Resize(new Point((int) outputLabel.Font.MeasureString(outputLabel.Text).X, outputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, outputLabelArea.RectTransform), style: "HorizontalLine"); @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components } else { - infoArea.Text = TextManager.Get(InfoText, returnNull: true) ?? InfoText; + infoArea.Text = TextManager.Get(InfoText).Fallback(InfoText); } if (IsActive) { @@ -137,11 +137,11 @@ namespace Barotrauma.Items.Components outputsFound = true; if (!string.IsNullOrEmpty(deconstructItem.ActivateButtonText)) { - string buttonText = TextManager.Get(deconstructItem.ActivateButtonText, returnNull: true) ?? deconstructItem.ActivateButtonText; - string infoText = string.Empty; + LocalizedString buttonText = TextManager.Get(deconstructItem.ActivateButtonText).Fallback(deconstructItem.ActivateButtonText); + LocalizedString infoText = string.Empty; if (!string.IsNullOrEmpty(deconstructItem.InfoText)) { - infoText = TextManager.Get(deconstructItem.InfoText, returnNull: true) ?? deconstructItem.InfoText; + infoText = TextManager.Get(deconstructItem.InfoText).Fallback(deconstructItem.InfoText); } inputItem.GetComponent()?.ModifyDeconstructInfo(this, ref buttonText, ref infoText); activateButton.Text = buttonText; @@ -159,7 +159,7 @@ namespace Barotrauma.Items.Components { if (deconstructItem.RequiredOtherItem.Any() && !string.IsNullOrEmpty(deconstructItem.InfoTextOnOtherItemMissing)) { - string missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First(), returnNull: true); + LocalizedString missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First()); infoArea.Text = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName); } } @@ -207,7 +207,7 @@ namespace Barotrauma.Items.Components overlayComponent.RectTransform.SetAsLastChild(); if (!(inputContainer?.Inventory?.visualSlots is { } visualSlots)) { return; } - + if (DeconstructItemsSimultaneously) { for (int i = 0; i < InputContainer.Inventory.Capacity; i++) @@ -227,13 +227,13 @@ namespace Barotrauma.Items.Components new Rectangle( slot.Rect.X, slot.Rect.Y + (int)(slot.Rect.Height * (1.0f - progressState)), slot.Rect.Width, (int)(slot.Rect.Height * progressState)), - GUI.Style.Green * 0.5f, isFilled: true); + GUIStyle.Green * 0.5f, isFilled: true); } } public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - inSufficientPowerWarning.Visible = CurrPowerConsumption > 0 && !hasPower; + inSufficientPowerWarning.Visible = IsActive && !hasPower; } private bool ToggleActive(GUIButton button, object obj) @@ -246,7 +246,6 @@ namespace Barotrauma.Items.Components else { SetActive(!IsActive, Character.Controlled); - currPowerConsumption = IsActive ? powerConsumption : 0.0f; } return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 159e1c3f9..89280fa88 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.85f, 0.65f), GuiFrame.RectTransform, Anchor.Center) { @@ -43,27 +43,27 @@ namespace Barotrauma.Items.Components powerIndicator = new GUITickBox(new RectTransform(new Vector2(0.45f, 0.8f), lightsArea.RectTransform, Anchor.Center, Pivot.CenterRight) { RelativeOffset = new Vector2(-0.05f, 0) - }, TextManager.Get("EnginePowered"), font: GUI.SubHeadingFont, style: "IndicatorLightGreen") + }, TextManager.Get("EnginePowered"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightGreen") { CanBeFocused = false }; autoControlIndicator = new GUITickBox(new RectTransform(new Vector2(0.45f, 0.8f), lightsArea.RectTransform, Anchor.Center, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0) - }, TextManager.Get("PumpAutoControl", fallBackTag: "ReactorAutoControl"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") + }, TextManager.Get("PumpAutoControl", "ReactorAutoControl"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightYellow") { Selected = false, Enabled = false, ToolTip = TextManager.Get("AutoControlTip") }; powerIndicator.TextBlock.Wrap = autoControlIndicator.TextBlock.Wrap = true; - powerIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - autoControlIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + autoControlIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, autoControlIndicator.TextBlock); var sliderArea = new GUIFrame(new RectTransform(new Vector2(1, 0.6f), paddedFrame.RectTransform, Anchor.BottomLeft), style: null); - string powerLabel = TextManager.Get("EngineForce"); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) + LocalizedString powerLabel = TextManager.Get("EngineForce"); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) { AutoScaleHorizontal = true, TextGetter = () => { return TextManager.AddPunctuation(':', powerLabel, (int)(targetForce) + " %"); } @@ -90,12 +90,12 @@ namespace Barotrauma.Items.Components var textsArea = new GUIFrame(new RectTransform(new Vector2(1, 0.25f), sliderArea.RectTransform, Anchor.BottomCenter), style: null); var backwardsLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), textsArea.RectTransform, Anchor.CenterLeft), TextManager.Get("EngineBackwards"), - textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); var forwardsLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), textsArea.RectTransform, Anchor.CenterRight), TextManager.Get("EngineForwards"), - textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight); + textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); GUITextBlock.AutoScaleAndNormalize(backwardsLabel, forwardsLabel); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components Vector2 drawPos = item.DrawPosition; drawPos += PropellerPos * item.Scale; drawPos.Y = -drawPos.Y; - spriteBatch.DrawCircle(drawPos, propellerDamage.DamageRange * item.Scale, 16, GUI.Style.Red, thickness: 2); + spriteBatch.DrawCircle(drawPos, propellerDamage.DamageRange * item.Scale, 16, GUIStyle.Red, thickness: 2); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 495315dff..4a3da093d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -37,7 +37,12 @@ namespace Barotrauma.Items.Components private FabricationRecipe pendingFabricatedItem; - private (Rectangle area, string text)? tooltip; + private class ToolTip + { + public Rectangle TargetElement; + public LocalizedString Tooltip; + } + private ToolTip tooltip; private GUITextBlock requiredTimeBlock; @@ -57,7 +62,7 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); // === LABEL === // - new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Name, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleVertical = true @@ -84,7 +89,7 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.03f, UserData = "filterarea" }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, AutoScaleVertical = true @@ -132,7 +137,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.03f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); @@ -154,7 +159,7 @@ namespace Barotrauma.Items.Components }; // === POWER WARNING === // inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("FabricatorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) + TextManager.Get("FabricatorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) { HoverColor = Color.Black, IgnoreLayoutGroups = true, @@ -168,7 +173,7 @@ namespace Barotrauma.Items.Components { itemList.Content.RectTransform.ClearChildren(); - foreach (FabricationRecipe fi in fabricationRecipes) + foreach (FabricationRecipe fi in fabricationRecipes.Values) { var frame = new GUIFrame(new RectTransform(new Point(itemList.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) { @@ -180,8 +185,8 @@ namespace Barotrauma.Items.Components var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f }; - - var itemIcon = fi.TargetItem.InventoryIcon ?? fi.TargetItem.sprite; + + var itemIcon = fi.TargetItem.InventoryIcon ?? fi.TargetItem.Sprite; if (itemIcon != null) { new GUIImage(new RectTransform(new Point(frame.Rect.Height,frame.Rect.Height), container.RectTransform), @@ -201,14 +206,13 @@ namespace Barotrauma.Items.Components } } - private string GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) + private LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) { if (fabricationRecipe == null) { return ""; } if (fabricationRecipe.Amount > 1) { return TextManager.GetWithVariables("fabricationrecipenamewithamount", - new string[2] { "[name]", "[amount]" }, - new string[2] { fabricationRecipe.DisplayName, fabricationRecipe.Amount.ToString() }); + ("[name]", fabricationRecipe.DisplayName), ("[amount]", fabricationRecipe.Amount.ToString())); } else { @@ -226,27 +230,6 @@ namespace Barotrauma.Items.Components partial void SelectProjSpecific(Character character) { - // TODO, This works fine as of now but if GUI.PreventElementOverlap ever gets fixed this block of code may become obsolete or detrimental. - // Only do this if there's only one linked component. If you link more containers then may - // GUI.PreventElementOverlap have mercy on your HUD layout - if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item { DisplaySideBySideWhenLinked: true }) == 1) - { - foreach (MapEntity linkedTo in item.linkedTo) - { - if (!(linkedTo is Item { DisplaySideBySideWhenLinked: true } linkedItem)) { continue; } - if (!linkedItem.Components.Any()) { continue; } - - var itemContainer = linkedItem.GetComponent(); - if (itemContainer?.GuiFrame == null || itemContainer.AllowUIOverlap) { continue; } - - // how much spacing do we want between the components - var padding = (int) (8 * GUI.Scale); - // Move the linked container to the right and move the fabricator to the left - itemContainer.GuiFrame.RectTransform.AbsoluteOffset = new Point(GuiFrame.Rect.Width / -2 - padding, 0); - GuiFrame.RectTransform.AbsoluteOffset = new Point(itemContainer.GuiFrame.Rect.Width / 2 + padding, 0); - } - } - var nonItems = itemList.Content.Children.Where(c => !(c.UserData is FabricationRecipe)).ToList(); nonItems.ForEach(i => itemList.Content.RemoveChild(i)); @@ -266,11 +249,11 @@ namespace Barotrauma.Items.Components return itemPlacement1 > itemPlacement2 ? -1 : 1; } - return string.Compare(item1.DisplayName, item2.DisplayName); + return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value); }); var sufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorsufficientskills"), textColor: GUI.Style.Green, font: GUI.SubHeadingFont) + TextManager.Get("fabricatorsufficientskills"), textColor: GUIStyle.Green, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, CanBeFocused = false @@ -278,8 +261,8 @@ namespace Barotrauma.Items.Components sufficientSkillsText.RectTransform.SetAsFirstChild(); var insufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUI.SubHeadingFont) - { + TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUIStyle.SubHeadingFont) + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -290,8 +273,8 @@ namespace Barotrauma.Items.Components } var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUI.SubHeadingFont) - { + TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUIStyle.SubHeadingFont) + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -330,7 +313,7 @@ namespace Barotrauma.Items.Components } foreach (Item item in inputContainer.Inventory.AllItems) { - missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.prefab))); + missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.Prefab))); } var missingCounts = missingItems.GroupBy(missingItem => missingItem).ToDictionary(x => x.Key, x => x.Count()); missingItems = missingItems.Distinct().ToList(); @@ -356,10 +339,10 @@ namespace Barotrauma.Items.Components if (availableSlotIndex < 0) { continue; } if (rootInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) { - rootInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + rootInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); if (slotIndex < inputContainer.Capacity) { - inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); } } } @@ -367,7 +350,7 @@ namespace Barotrauma.Items.Components if (slotIndex >= inputContainer.Capacity) { break; } - var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().sprite; + var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().Sprite; Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect; itemIcon.Draw( spriteBatch, @@ -380,9 +363,9 @@ namespace Barotrauma.Items.Components { Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom); string stackCountText = "x" + missingCounts[requiredItem]; - stackCountPos -= GUI.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + stackCountPos -= GUIStyle.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) @@ -401,13 +384,13 @@ namespace Barotrauma.Items.Components GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, slotRect.Width - spacing * 2, height), Color.Black * 0.8f, true); GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, (int)((slotRect.Width - spacing * 2) * condition), height), - GUI.Style.Green * 0.8f, true); + GUIStyle.Green * 0.8f, true); } if (slotRect.Contains(PlayerInput.MousePosition)) { var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name); - string toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); + LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); if (suitableIngredients.Count() > 3) { toolTipText += "..."; } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { @@ -428,11 +411,11 @@ namespace Barotrauma.Items.Components { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); } - if (!string.IsNullOrEmpty(requiredItem.ItemPrefabs.First().Description)) + if (!requiredItem.ItemPrefabs.First().Description.IsNullOrEmpty()) { toolTipText += '\n' + requiredItem.ItemPrefabs.First().Description; } - tooltip = (slotRect, toolTipText); + tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText }; } slotIndex++; @@ -456,12 +439,12 @@ namespace Barotrauma.Items.Components new Rectangle( slotRect.X, slotRect.Y + (int)(slotRect.Height * (1.0f - clampedProgressState)), slotRect.Width, (int)(slotRect.Height * clampedProgressState)), - GUI.Style.Green * 0.5f, isFilled: true); + GUIStyle.Green * 0.5f, isFilled: true); } if (outputContainer.Inventory.IsEmpty()) { - var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.sprite; + var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.Sprite; itemIcon.Draw( spriteBatch, slotRect.Center.ToVector2(), @@ -472,7 +455,7 @@ namespace Barotrauma.Items.Components if (tooltip != null) { - GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.text, tooltip.Value.area); + GUIComponent.DrawToolTip(spriteBatch, tooltip.Tooltip, tooltip.TargetElement); tooltip = null; } } @@ -485,12 +468,11 @@ namespace Barotrauma.Items.Components return true; } - filter = filter.ToLower(); foreach (GUIComponent child in itemList.Content.Children) { FabricationRecipe recipe = child.UserData as FabricationRecipe; if (recipe?.DisplayName == null) { continue; } - child.Visible = recipe.DisplayName.ToLower().Contains(filter); + child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase); } HideEmptyItemListCategories(); @@ -538,24 +520,26 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; - string itemName = GetRecipeNameAndAmount(selectedItem); - string name = itemName; + LocalizedString itemName = GetRecipeNameAndAmount(selectedItem); + LocalizedString name = itemName; float quality = GetFabricatedItemQuality(selectedItem, user); if (quality > 0) { - name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n', fallBackTag: "itemname.quality3"); + name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n') + .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n')); } var nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - name, textAlignment: Alignment.TopLeft, textColor: Color.Aqua, font: GUI.SubHeadingFont, parseRichText: true) + RichString.Rich(name), textAlignment: Alignment.TopLeft, textColor: Color.Aqua, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W); if (nameBlock.TextScale < 0.7f) { - nameBlock.SetRichText(TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName, fallBackTag: "itemname.quality3")); + nameBlock.SetRichText(TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName) + .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName))); nameBlock.AutoScaleHorizontal = false; nameBlock.TextScale = 0.7f; nameBlock.Wrap = true; @@ -563,35 +547,35 @@ namespace Barotrauma.Items.Components nameBlock.RectTransform.MinSize = new Point(0, (int)(nameBlock.TextSize.Y * nameBlock.TextScale)); } - if (!string.IsNullOrWhiteSpace(selectedItem.TargetItem.Description)) + if (!selectedItem.TargetItem.Description.IsNullOrEmpty()) { var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), selectedItem.TargetItem.Description, - font: GUI.SmallFont, wrap: true); + font: GUIStyle.SmallFont, wrap: true); description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W); while (description.Rect.Height + nameBlock.Rect.Height > paddedFrame.Rect.Height) { var lines = description.WrappedText.Split('\n'); - if (lines.Length <= 1) { break; } - var newString = string.Join('\n', lines.Take(lines.Length - 1)); + if (lines.Count <= 1) { break; } + var newString = string.Join('\n', lines.Take(lines.Count - 1)); description.Text = newString.Substring(0, newString.Length - 4) + "..."; description.CalculateHeightFromText(); description.ToolTip = selectedItem.TargetItem.Description; } } - List inadequateSkills = new List(); + IEnumerable inadequateSkills = Enumerable.Empty(); if (user != null) { - inadequateSkills = selectedItem.RequiredSkills.FindAll(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier)); + inadequateSkills = selectedItem.RequiredSkills.Where(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier)); } if (selectedItem.RequiredSkills.Any()) { - string text = ""; + LocalizedString text = ""; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), - TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUI.Style.Red : GUI.Style.Green, font: GUI.SubHeadingFont) + TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, }; @@ -600,7 +584,7 @@ namespace Barotrauma.Items.Components text += TextManager.Get("SkillName." + skill.Identifier) + " " + TextManager.Get("Lvl").ToLower() + " " + Math.Round(skill.Level * SkillRequirementMultiplier); if (skill != selectedItem.RequiredSkills.Last()) { text += "\n"; } } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), text, font: GUI.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), text, font: GUIStyle.SmallFont); } float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills); @@ -610,13 +594,13 @@ namespace Barotrauma.Items.Components (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), - TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUI.Style.Red, Color.Yellow, GUI.Style.Green), font: GUI.SubHeadingFont) + TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, }; requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); return true; } @@ -649,7 +633,7 @@ namespace Barotrauma.Items.Components if (fabricatedItem == null && !outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { - outputSlot.Flash(GUI.Style.Red); + outputSlot.Flash(GUIStyle.Red); return false; } @@ -676,7 +660,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { activateButton.Enabled = false; - inSufficientPowerWarning.Visible = currPowerConsumption > 0 && !hasPower; + inSufficientPowerWarning.Visible = IsActive && !hasPower; if (!IsActive) { @@ -723,31 +707,31 @@ namespace Barotrauma.Items.Components public void ClientWrite(IWriteMessage msg, object[] extraData = null) { - int itemIndex = pendingFabricatedItem == null ? -1 : fabricationRecipes.IndexOf(pendingFabricatedItem); - msg.WriteRangedInteger(itemIndex, -1, fabricationRecipes.Count - 1); + uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0; + msg.Write(recipeHash); } public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { FabricatorState newState = (FabricatorState)msg.ReadByte(); float newTimeUntilReady = msg.ReadSingle(); - int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1); + uint recipeHash = msg.ReadUInt32(); UInt16 userID = msg.ReadUInt16(); Character user = Entity.FindEntityByID(userID) as Character; State = newState; - if (newState == FabricatorState.Stopped || itemIndex == -1) + if (newState == FabricatorState.Stopped || recipeHash == 0) { CancelFabricating(); } else if (newState == FabricatorState.Active || newState == FabricatorState.Paused) { //if already fabricating the selected item, return - if (fabricatedItem != null && fabricationRecipes.IndexOf(fabricatedItem) == itemIndex) { return; } - if (itemIndex < 0 || itemIndex >= fabricationRecipes.Count) { return; } + if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; } + if (recipeHash == 0) { return; } - SelectItem(user, fabricationRecipes[itemIndex]); - StartFabricating(fabricationRecipes[itemIndex], user); + SelectItem(user, fabricationRecipes[recipeHash]); + StartFabricating(fabricationRecipes[recipeHash], user); } timeUntilReady = newTimeUntilReady; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index b8464aec8..a1ea10952 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components { private GUIFrame submarineContainer; - private GUIFrame hullInfoFrame; + private GUIFrame? hullInfoFrame; private GUIScissorComponent? scissorComponent; private GUIComponent? miniMapContainer; private GUIComponent miniMapFrame; @@ -160,7 +160,7 @@ namespace Barotrauma.Items.Components private GUITextBlock tooltipHeader, tooltipFirstLine, tooltipSecondLine, tooltipThirdLine; - private string noPowerTip = string.Empty; + private LocalizedString noPowerTip = string.Empty; private readonly List displayedSubs = new List(); @@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components public static readonly Color MiniMapBaseColor = new Color(15, 178, 107); private static readonly Color WetHullColor = new Color(11, 122, 205), - DoorIndicatorColor = GUI.Style.Green, + DoorIndicatorColor = GUIStyle.Green, NoPowerDoorColor = DoorIndicatorColor * 0.1f, DefaultNeutralColor = MiniMapBaseColor * 0.8f, HoverColor = Color.White, @@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components HullWaterColor = new Color(17, 173, 179) * 0.5f, HullWaterLineColor = Color.LightBlue * 0.5f, NoPowerColor = MiniMapBaseColor * 0.1f, - ElectricalBaseColor = GUI.Style.Orange, + ElectricalBaseColor = GUIStyle.Orange, NoPowerElectricalColor = ElectricalBaseColor * 0.1f; partial void InitProjSpecific() @@ -292,7 +292,7 @@ namespace Barotrauma.Items.Components } } - List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -414,7 +414,7 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList(int order = 0) { base.AddToGUIUpdateList(order); - hullInfoFrame.AddToGUIUpdateList(order: order + 1); + hullInfoFrame?.AddToGUIUpdateList(order: order + 1); if (currentMode == MiniMapMode.ItemFinder && searchBar.Selected) { searchAutoComplete?.AddToGUIUpdateList(order: order + 1); @@ -507,7 +507,7 @@ namespace Barotrauma.Items.Components { Vector2 origin = weaponSprite.Origin; float scale = parentWidth / Math.Max(weaponSprite.size.X, weaponSprite.size.Y); - Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUI.Style.Green; + Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green; weaponSprite.Draw(batch, center, color, origin, rotation, scale, it.SpriteEffects); } }); @@ -556,7 +556,7 @@ namespace Barotrauma.Items.Components dragMapStart = PlayerInput.MousePosition; } } - + if (currentMode != MiniMapMode.HullStatus && Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))) { float newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom); @@ -664,11 +664,11 @@ namespace Barotrauma.Items.Components { if (Voltage < MinVoltage) { - Vector2 textSize = GUI.Font.MeasureString(noPowerTip); + Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); - Color noPowerColor = GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)); + Color noPowerColor = GUIStyle.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)); - GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, noPowerColor, Color.Black * 0.8f, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, noPowerColor, Color.Black * 0.8f, font: GUIStyle.SubHeadingFont); return; } @@ -679,7 +679,7 @@ namespace Barotrauma.Items.Components spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; - var sprite = GUI.Style.UIGlowSolidCircular?.Sprite; + var sprite = GUIStyle.UIGlowSolidCircular.Value?.Sprite; float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; if (sprite != null) { @@ -693,7 +693,7 @@ namespace Barotrauma.Items.Components Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f; - Color color = ToolBox.GradientLerp(gap.Open, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorLow) * alpha; + Color color = ToolBox.GradientLerp(gap.Open, GUIStyle.HealthBarColorMedium, GUIStyle.HealthBarColorLow) * alpha; sprite.Draw(spriteBatch, miniMapFrame.Rect.Location.ToVector2() + entityRect.Center, color, origin: sprite.Origin, rotate: 0.0f, scale: scale); @@ -710,7 +710,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull is { } currentHull && currentHull == hull) { - Sprite? pingCircle = GUI.Style.YouAreHereCircle?.Sprite; + Sprite pingCircle = GUIStyle.YouAreHereCircle.Value.Sprite; if (pingCircle is null) { continue; } Vector2 charPos = item.WorldPosition; @@ -725,7 +725,7 @@ namespace Barotrauma.Items.Components Vector2 drawPos = component.RectComponent.Rect.Location.ToVector2() + relativePos; drawPos -= new Vector2(spriteSize, spriteSize) / 2f; - pingCircle.Draw(spriteBatch, drawPos, GUI.Style.Red * 0.8f, Vector2.Zero, 0f, parentWidth / pingCircle.size.X); + pingCircle.Draw(spriteBatch, drawPos, GUIStyle.Red * 0.8f, Vector2.Zero, 0f, parentWidth / pingCircle.size.X); } } } @@ -805,7 +805,7 @@ namespace Barotrauma.Items.Components private void CreateItemFrame(ItemPrefab prefab, RectTransform parent) { - Sprite sprite = prefab.InventoryIcon ?? prefab.sprite; + Sprite sprite = prefab.InventoryIcon ?? prefab.Sprite; if (sprite is null) { return; } GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent), style: "ListBoxElement") { @@ -821,7 +821,7 @@ namespace Barotrauma.Items.Components Color = prefab.InventoryIconColor, UserData = prefab }; - + var nameText = new GUITextBlock(new RectTransform(Vector2.One, layout.RectTransform), prefab.Name); nameText.RectTransform.SizeChanged += () => { @@ -837,7 +837,7 @@ namespace Barotrauma.Items.Components if (first is null) { - searchBar.Flash(GUI.Style.Red); + searchBar.Flash(GUIStyle.Red); return; } searchedPrefab = first; @@ -890,7 +890,7 @@ namespace Barotrauma.Items.Components { if (item.Submarine == null) { return; } - hullInfoFrame.Visible = false; + if (hullInfoFrame != null) { hullInfoFrame.Visible = false; } reportFrame.Visible = false; searchBarFrame.Visible = false; electricalFrame.Visible = false; @@ -1029,7 +1029,7 @@ namespace Barotrauma.Items.Components { float amount = 1f + hullData.LinkedHulls.Count; gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom && !g.HiddenInGame).Sum(g => g.Open) / amount; - borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min(gapOpenSum, 1.0f)); + borderColor = Color.Lerp(neutralColor, GUIStyle.Red, Math.Min(gapOpenSum, 1.0f)); } bool isHoveringOver = canHoverOverHull && GUI.MouseOn == component; @@ -1037,28 +1037,28 @@ namespace Barotrauma.Items.Components // When drawing tooltip we are only interested in the component we are hovering over if (isHoveringOver) { - string header = hull.DisplayName; + LocalizedString header = hull.DisplayName; float? oxygenAmount = hullData.HullOxygenAmount, waterAmount = hullData.HullWaterAmount; - string line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; - Color line1Color = GUI.Style.Red; + LocalizedString line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; + Color line1Color = GUIStyle.Red; - string line2 = oxygenAmount == null ? + LocalizedString line2 = oxygenAmount == null ? TextManager.Get("MiniMapAirQualityUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), (int)Math.Round(oxygenAmount.Value) + "%"); - Color line2Color = oxygenAmount == null ? GUI.Style.Red : Color.Lerp(GUI.Style.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); + Color line2Color = oxygenAmount == null ? GUIStyle.Red : Color.Lerp(GUIStyle.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); - string line3 = waterAmount == null ? + LocalizedString line3 = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value * 100.0f) + "%"); - Color line3Color = waterAmount == null ? GUI.Style.Red : Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)waterAmount); + Color line3Color = waterAmount == null ? GUIStyle.Red : Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)waterAmount); SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color); } - bool draggingReport = GameMain.GameSession?.CrewManager?.DraggedOrder != null; + bool draggingReport = GameMain.GameSession?.CrewManager?.DraggedOrderPrefab != null; // When setting the colors we want to know the linked hulls too or else the linked hull will not realize its being hovered over and reset the border color foreach (Hull linkedHull in hullData.LinkedHulls) { @@ -1090,7 +1090,7 @@ namespace Barotrauma.Items.Components foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) { if (!(entity is Item it)) { continue; } - if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent component)) { continue; } + if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent? component)) { continue; } if (entity.Removed) { @@ -1106,12 +1106,12 @@ namespace Barotrauma.Items.Components if (Voltage < MinVoltage || !miniMapGuiComponent.RectComponent.Visible) { continue; } int durability = (int)(it.Condition / (it.MaxCondition / it.MaxRepairConditionMultiplier) * 100f); - Color color = ToolBox.GradientLerp(durability / 100f, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green, GUI.Style.Green); + Color color = ToolBox.GradientLerp(durability / 100f, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green, GUIStyle.Green); if (GUI.MouseOn == component) { - string line1 = string.Empty; - string line2 = string.Empty; + LocalizedString line1 = string.Empty; + LocalizedString line2 = string.Empty; if (it.GetComponent() is { } battery) { @@ -1120,13 +1120,21 @@ namespace Barotrauma.Items.Components } else if (it.GetComponent() is { } powerTransfer) { - int current = (int)-powerTransfer.CurrPowerConsumption, load = (int)powerTransfer.PowerLoad; + int current = 0, load = 0; + if (powerTransfer.PowerConnections.Count > 0 && powerTransfer.PowerConnections[0].Grid != null) + { + current = (int)powerTransfer.PowerConnections[0].Grid.Power; + load = (int)powerTransfer.PowerConnections[0].Grid.Load; + } - line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString(), fallBackTag: "statusmonitor.junctioncurrent.tooltip"); - line2 = TextManager.GetWithVariables("statusmonitor.junctionload.tooltip", new string[] { "[amount]", "[load]" }, new string[] { load.ToString(), load.ToString() }); + line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString()) + .Fallback(TextManager.GetWithVariable("statusmonitor.junctioncurrent.tooltip", "[amount]", current.ToString())); + line2 = TextManager.GetWithVariables("statusmonitor.junctionload.tooltip", + ("[amount]", load.ToString()), + ("[load]", load.ToString())); } - string line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); + LocalizedString line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); SetTooltip(component.Rect.Center, it.Prefab.Name, line1, line2, line3, line3Color: color); color = HoverColor; } @@ -1154,12 +1162,12 @@ namespace Barotrauma.Items.Components foreach (Vector2 blip in MiniMapBlips) { Vector2 parentSize = miniMapFrame.Rect.Size.ToVector2(); - Sprite pingCircle = GUI.Style.PingCircle.Sprite; + Sprite pingCircle = GUIStyle.PingCircle.Value.Sprite; Vector2 targetSize = new Vector2(parentSize.X / 4f); Vector2 spriteScale = targetSize / pingCircle.size; float scale = Math.Min(blipState, maxBlipState / 2f); float alpha = 1.0f - Math.Clamp((blipState - maxBlipState * 0.25f) * 2f, 0f, 1f); - pingCircle.Draw(spriteBatch, electricalFrame.Rect.Location.ToVector2() + blip * Zoom, GUI.Style.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); + pingCircle.Draw(spriteBatch, electricalFrame.Rect.Location.ToVector2() + blip * Zoom, GUIStyle.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); } } } @@ -1199,7 +1207,7 @@ namespace Barotrauma.Items.Components if (hullsVisible && hullData.HullOxygenAmount is { } oxygenAmount) { - GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, oxygenAmount / 100.0f), true); + GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUIStyle.Red * 0.5f, GUIStyle.Green * 0.3f, oxygenAmount / 100.0f), true); } } } @@ -1210,8 +1218,9 @@ namespace Barotrauma.Items.Components spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - private void SetTooltip(Point pos, string header, string line1, string line2, string line3, Color? line1Color = null, Color? line2Color = null, Color? line3Color = null) + private void SetTooltip(Point pos, LocalizedString header, LocalizedString line1, LocalizedString line2, LocalizedString line3, Color? line1Color = null, Color? line2Color = null, Color? line3Color = null) { + if (hullInfoFrame == null) { return; } hullInfoFrame.RectTransform.ScreenSpaceOffset = pos; if (hullInfoFrame.Rect.Left > submarineContainer.Rect.Right) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(submarineContainer.Rect.Right, hullInfoFrame.RectTransform.ScreenSpaceOffset.Y); } @@ -1224,13 +1233,13 @@ namespace Barotrauma.Items.Components tooltipHeader.Text = header; tooltipFirstLine.Text = line1; - tooltipFirstLine.TextColor = line1Color ?? GUI.Style.TextColor; + tooltipFirstLine.TextColor = line1Color ?? GUIStyle.TextColorNormal; tooltipSecondLine.Text = line2; - tooltipSecondLine.TextColor = line2Color ?? GUI.Style.TextColor; + tooltipSecondLine.TextColor = line2Color ?? GUIStyle.TextColorNormal; tooltipThirdLine.Text = line3; - tooltipThirdLine.TextColor = line3Color ?? GUI.Style.TextColor; + tooltipThirdLine.TextColor = line3Color ?? GUIStyle.TextColorNormal; } private void BakeSubmarine(Submarine sub, Rectangle container) @@ -1355,9 +1364,9 @@ namespace Barotrauma.Items.Components if (GameMain.GameSession?.CrewManager is { ActiveOrders: { } orders }) { - foreach (var pair in orders) + foreach (var activeOrder in orders) { - Order order = pair.First; + Order order = activeOrder.Order; if (order is { SymbolSprite: { }, TargetEntity: Hull _ } && order.TargetEntity == hull) { cardsToDraw.Add(new MiniMapSprite(order)); @@ -1367,7 +1376,7 @@ namespace Barotrauma.Items.Components foreach (IdCard card in data.Cards) { - if (card.GetJob() is { Icon: { }} job) + if (card.OwnerJob is { Icon: { }} job) { cardsToDraw.Add(new MiniMapSprite(job)); } @@ -1415,17 +1424,17 @@ namespace Barotrauma.Items.Components if (amountLeft > 0) { string text = $"+{amountLeft}"; // TODO localization - var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(text); // TODO expensive, move to a global variable + var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(text); // TODO expensive, move to a global variable float maxWidth = Math.Max(sizeX, sizeY); Vector2 drawPos = new Vector2(frame.Rect.Right - sizeX, frame.Rect.Y - sizeY / 2f); - UISprite icon = GUI.Style.IconOverflowIndicator; + UISprite icon = GUIStyle.IconOverflowIndicator; if (icon != null) { const int iconPadding = 4; - icon.Draw(spriteBatch, new Rectangle((int) drawPos.X - iconPadding, (int) drawPos.Y - iconPadding, (int) maxWidth + iconPadding * 2, (int) maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); + icon.Draw(spriteBatch, new Rectangle((int)drawPos.X - iconPadding, (int)drawPos.Y - iconPadding, (int)maxWidth + iconPadding * 2, (int)maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); } - GUI.DrawString(spriteBatch, drawPos, text, GUI.Style.TextColor, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, drawPos, text, GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont); } break; } @@ -1485,7 +1494,7 @@ namespace Barotrauma.Items.Components if (settings.CreateHullElements) { - hullList = Hull.hullList.Where(IsPartofSub).ToImmutableArray(); + hullList = Hull.HullList.Where(IsPartofSub).ToImmutableArray(); combinedHulls = CombinedHulls(hullList); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index d0bf73438..c65e6c41a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -19,9 +19,9 @@ namespace Barotrauma.Items.Components private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpOutEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpInEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -47,12 +47,12 @@ namespace Barotrauma.Items.Components var paddedPowerArea = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.8f), powerArea.RectTransform, Anchor.Center), style: "PowerButtonFrame"); var powerLightArea = new GUIFrame(new RectTransform(new Vector2(0.87f, 0.2f), powerArea.RectTransform, Anchor.TopRight), style: null); powerLight = new GUITickBox(new RectTransform(Vector2.One, powerLightArea.RectTransform, Anchor.Center), - TextManager.Get("PowerLabel"), font: GUI.SubHeadingFont, style: "IndicatorLightPower") + TextManager.Get("PowerLabel"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightPower") { CanBeFocused = false }; powerLight.TextBlock.AutoScaleHorizontal = true; - powerLight.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerLight.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); PowerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.75f), paddedPowerArea.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0, 0.1f) @@ -75,23 +75,23 @@ namespace Barotrauma.Items.Components var rightArea = new GUIFrame(new RectTransform(new Vector2(0.65f, 1), paddedFrame.RectTransform, Anchor.CenterRight), style: null); autoControlIndicator = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), rightArea.RectTransform, Anchor.TopLeft), - TextManager.Get("PumpAutoControl", fallBackTag: "ReactorAutoControl"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") + TextManager.Get("PumpAutoControl", "ReactorAutoControl"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightYellow") { Selected = false, Enabled = false, ToolTip = TextManager.Get("AutoControlTip") }; autoControlIndicator.TextBlock.AutoScaleHorizontal = true; - autoControlIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + autoControlIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); var sliderArea = new GUIFrame(new RectTransform(new Vector2(1, 0.65f), rightArea.RectTransform, Anchor.BottomLeft), style: null); var pumpSpeedText = new GUITextBlock(new RectTransform(new Vector2(1, 0.3f), sliderArea.RectTransform, Anchor.TopLeft), "", - textColor: GUI.Style.TextColor, textAlignment: Alignment.CenterLeft, wrap: false, font: GUI.SubHeadingFont) + textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.CenterLeft, wrap: false, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; - string pumpSpeedStr = TextManager.Get("PumpSpeed"); - pumpSpeedText.TextGetter = () => { return TextManager.AddPunctuation(':', pumpSpeedStr, (int)flowPercentage + " %"); }; + LocalizedString pumpSpeedStr = TextManager.Get("PumpSpeed"); + pumpSpeedText.TextGetter = () => { return TextManager.AddPunctuation(':', pumpSpeedStr, (int)Math.Round(flowPercentage) + " %"); }; pumpSpeedSlider = new GUIScrollBar(new RectTransform(new Vector2(1, 0.35f), sliderArea.RectTransform, Anchor.Center), barSize: 0.1f, style: "DeviceSlider") { Step = 0.05f, @@ -116,9 +116,9 @@ namespace Barotrauma.Items.Components }; var textsArea = new GUIFrame(new RectTransform(new Vector2(1, 0.25f), sliderArea.RectTransform, Anchor.BottomCenter), style: null); var outLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textsArea.RectTransform, Anchor.CenterLeft), TextManager.Get("PumpOut"), - textColor: GUI.Style.TextColor, textAlignment: Alignment.CenterLeft, wrap: false, font: GUI.SubHeadingFont); + textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.CenterLeft, wrap: false, font: GUIStyle.SubHeadingFont); var inLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textsArea.RectTransform, Anchor.CenterRight), TextManager.Get("PumpIn"), - textColor: GUI.Style.TextColor, textAlignment: Alignment.CenterRight, wrap: false, font: GUI.SubHeadingFont); + textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.CenterRight, wrap: false, font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(outLabel, inLabel); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index aa5e644f5..6627d8491 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components "ReactorWarningOverheating", "ReactorWarningHighOutput", "ReactorWarningFuelOut", "ReactorWarningSCRAM" }; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { // TODO: need to recreate the gui when the resolution changes @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components }; /*new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), inventoryContent.RectTransform), "", - textAlignment: Alignment.Center, font: GUI.SubHeadingFont, wrap: true);*/ + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true);*/ inventoryContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), inventoryContent.RectTransform), style: null); //---------------------------------------------------------- @@ -131,28 +131,28 @@ namespace Barotrauma.Items.Components Point maxIndicatorSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)); criticalHeatWarning = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, - TextManager.Get("ReactorWarningCriticalTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("ReactorWarningCriticalTemp"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorHeatTip") }; criticalOutputWarning = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, - TextManager.Get("ReactorWarningCriticalOutput"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("ReactorWarningCriticalOutput"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorOutputTip") }; lowTemperatureWarning = new GUITickBox(new RectTransform(new Vector2(0.4f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, - TextManager.Get("ReactorWarningCriticalLowTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("ReactorWarningCriticalLowTemp"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorTempTip") }; List indicatorLights = new List() { criticalHeatWarning, lowTemperatureWarning, criticalOutputWarning }; - indicatorLights.ForEach(l => l.TextBlock.OverrideTextColor(GUI.Style.TextColor)); + indicatorLights.ForEach(l => l.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal)); topLeftArea.Recalculate(); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), columnLeft.RectTransform), style: "HorizontalLine"); @@ -167,7 +167,7 @@ namespace Barotrauma.Items.Components var rightArea = new GUIFrame(new RectTransform(new Vector2(0.49f, 1), meterArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), style: null); var fissionRateTextBox = new GUITextBlock(new RectTransform(relativeTextSize, leftArea.RectTransform, Anchor.TopCenter), - TextManager.Get("ReactorFissionRate"), textColor: GUI.Style.TextColor, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + TextManager.Get("ReactorFissionRate"), textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components }; var turbineOutputTextBox = new GUITextBlock(new RectTransform(relativeTextSize, rightArea.RectTransform, Anchor.TopCenter), - TextManager.Get("ReactorTurbineOutput"), textColor: GUI.Style.TextColor, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + TextManager.Get("ReactorTurbineOutput"), textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; @@ -254,7 +254,7 @@ namespace Barotrauma.Items.Components var b = new GUIButton(new RectTransform(Vector2.One, (i < 4) ? upperButtons.RectTransform : lowerButtons.RectTransform), TextManager.Get(text), style: "IndicatorButton") { - Font = GUI.SubHeadingFont, + Font = GUIStyle.SubHeadingFont, CanBeFocused = false }; warningButtons.Add(text, b); @@ -298,14 +298,14 @@ namespace Barotrauma.Items.Components AutoTempSwitch.RectTransform.MaxSize = new Point((int)(AutoTempSwitch.Rect.Height * 0.4f), int.MaxValue); autoTempLight = new GUITickBox(new RectTransform(new Vector2(0.4f, 1.0f), topRightArea.RectTransform), - TextManager.Get("ReactorAutoTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") + TextManager.Get("ReactorAutoTemp"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightYellow") { ToolTip = TextManager.Get("ReactorTipAutoTemp"), CanBeFocused = false, Selected = AutoTemp }; autoTempLight.RectTransform.MaxSize = new Point(int.MaxValue, criticalHeatWarning.Rect.Height); - autoTempLight.TextBlock.OverrideTextColor(GUI.Style.TextColor); + autoTempLight.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); new GUIFrame(new RectTransform(new Vector2(0.01f, 1.0f), topRightArea.RectTransform), style: "VerticalLine"); @@ -313,14 +313,14 @@ namespace Barotrauma.Items.Components var powerArea = new GUIFrame(new RectTransform(new Vector2(0.4f, 1.0f), topRightArea.RectTransform), style: null); var paddedPowerArea = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), powerArea.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: "PowerButtonFrame"); powerLight = new GUITickBox(new RectTransform(new Vector2(0.87f, 0.3f), paddedPowerArea.RectTransform, Anchor.TopCenter, Pivot.Center), - TextManager.Get("PowerLabel"), font: GUI.SubHeadingFont, style: "IndicatorLightPower") + TextManager.Get("PowerLabel"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightPower") { CanBeFocused = false, Selected = _powerOn }; powerLight.TextBlock.Padding = new Vector4(5.0f, 0.0f, 0.0f, 0.0f); powerLight.TextBlock.AutoScaleHorizontal = true; - powerLight.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerLight.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); PowerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.75f), paddedPowerArea.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0, 0.1f) @@ -337,7 +337,7 @@ namespace Barotrauma.Items.Components topRightArea.Recalculate(); autoTempLight.TextBlock.Padding = new Vector4(autoTempLight.TextBlock.Padding.X, 0.0f, 0.0f, 0.0f); - autoTempLight.TextBlock.Text = autoTempLight.TextBlock.Text.Replace(' ', '\n'); + autoTempLight.TextBlock.Text = autoTempLight.TextBlock.Text.Replace(" ", "\n"); autoTempLight.TextBlock.AutoScaleHorizontal = true; GUITextBlock.AutoScaleAndNormalize(indicatorLights.Select(l => l.TextBlock)); @@ -364,23 +364,23 @@ namespace Barotrauma.Items.Components relativeTextSize = new Vector2(1.0f, 0.15f); var loadText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform), - "Load", textColor: loadColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + "Load", textColor: loadColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("ReactorTipLoad") }; - string loadStr = TextManager.Get("ReactorLoad"); - string kW = TextManager.Get("kilowatt"); + LocalizedString loadStr = TextManager.Get("ReactorLoad"); + LocalizedString kW = TextManager.Get("kilowatt"); loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}"; - + var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed"); new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null); var outputText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform), - "Output", textColor: outputColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + "Output", textColor: outputColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("ReactorTipPower") }; - string outputStr = TextManager.Get("ReactorOutput"); + LocalizedString outputStr = TextManager.Get("ReactorOutput"); outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}"; } @@ -610,7 +610,7 @@ namespace Barotrauma.Items.Components if (optimalRangeNormalized.X == optimalRangeNormalized.Y) { - sectorSprite.Draw(spriteBatch, pointerPos, GUI.Style.Red, MathHelper.PiOver2, scale); + sectorSprite.Draw(spriteBatch, pointerPos, GUIStyle.Red, MathHelper.PiOver2, scale); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index efda23637..8b549c0b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -56,7 +56,7 @@ namespace Barotrauma.Items.Components private Sprite sonarBlip; private Sprite lineSprite; - private readonly Dictionary> targetIcons = new Dictionary>(); + private readonly Dictionary> targetIcons = new Dictionary>(); private float displayBorderSize; @@ -130,10 +130,10 @@ namespace Barotrauma.Items.Components private bool isConnectedToSteering; - private static string caveLabel; + private static LocalizedString caveLabel; - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.Yes)] public bool RightLayout { get; @@ -143,16 +143,16 @@ namespace Barotrauma.Items.Components private bool AllowUsingMineralScanner => HasMineralScanner && !isConnectedToSteering; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast().All(t => blipColorGradient.ContainsKey(t))); sonarBlips = new List(); - caveLabel = - TextManager.Get("cave", returnNull: true) ?? - TextManager.Get("missiontype.nest"); + caveLabel = + TextManager.Get("cave").Fallback( + TextManager.Get("missiontype.nest")); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components case "icon": var targetIconSprite = new Sprite(subElement); var color = subElement.GetAttributeColor("color", Color.White); - targetIcons.Add(subElement.GetAttributeString("identifier", ""), + targetIcons.Add(subElement.GetAttributeIdentifier("identifier", Identifier.Empty), new Tuple(targetIconSprite, color)); break; } @@ -238,21 +238,21 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(SonarModeSwitch.RectTransform.RelativeSize.X, 0) }, style: null); passiveTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.TopLeft), - TextManager.Get("SonarPassive"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SonarPassive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { ToolTip = TextManager.Get("SonarTipPassive"), Selected = true, Enabled = false }; activeTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.BottomLeft), - TextManager.Get("SonarActive"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SonarActive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { ToolTip = TextManager.Get("SonarTipActive"), Selected = false, Enabled = false }; - passiveTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); - activeTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); + passiveTickBox.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + activeTickBox.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); textBlocksToScaleAndNormalize.Clear(); textBlocksToScaleAndNormalize.Add(passiveTickBox.TextBlock); @@ -261,7 +261,7 @@ namespace Barotrauma.Items.Components lowerAreaFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.BottomCenter), style: null); var zoomContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.TopCenter), style: null); var zoomText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.6f), zoomContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get("SonarZoom"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight); + TextManager.Get("SonarZoom"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); textBlocksToScaleAndNormalize.Add(zoomText); zoomSlider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 0.8f), zoomContainer.RectTransform, Anchor.CenterLeft) { @@ -299,7 +299,7 @@ namespace Barotrauma.Items.Components } }; var directionalModeSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), directionalModeFrame.RectTransform, Anchor.CenterRight), - TextManager.Get("SonarDirectionalPing"), GUI.Style.TextColor, GUI.SubHeadingFont, Alignment.CenterLeft); + TextManager.Get("SonarDirectionalPing"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(directionalModeSwitchText); if (AllowUsingMineralScanner) @@ -319,7 +319,7 @@ namespace Barotrauma.Items.Components (spriteBatch, guiCustomComponent) => { DrawSonar(spriteBatch, guiCustomComponent.Rect); }, null); signalWarningText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), sonarView.RectTransform, Anchor.Center, Pivot.BottomCenter), - "", warningColor, GUI.LargeFont, Alignment.Center); + "", warningColor, GUIStyle.LargeFont, Alignment.Center); // Setup layout for nav terminal if (isConnectedToSteering || RightLayout) @@ -421,7 +421,7 @@ namespace Barotrauma.Items.Components } }; var mineralScannerSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), mineralScannerFrame.RectTransform, Anchor.CenterRight), - TextManager.Get("SonarMineralScanner"), GUI.Style.TextColor, GUI.SubHeadingFont, Alignment.CenterLeft); + TextManager.Get("SonarMineralScanner"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(mineralScannerSwitchText); } @@ -570,7 +570,7 @@ namespace Barotrauma.Items.Components { levelTriggerFlows.Add(trigger, flow); } - if (!string.IsNullOrWhiteSpace(trigger.InfectIdentifier) && + if (!trigger.InfectIdentifier.IsEmpty && Vector2.DistanceSquared(transducerCenter, trigger.WorldPosition) < pingRange / 2 * pingRange / 2) { ballastFloraSpores.Add(trigger); @@ -918,12 +918,12 @@ namespace Barotrauma.Items.Components foreach (AITarget aiTarget in AITarget.List) { if (aiTarget.InDetectable) { continue; } - if (string.IsNullOrEmpty(aiTarget.SonarLabel) || aiTarget.SoundRange <= 0.0f) { continue; } + if (aiTarget.SonarLabel.IsNullOrEmpty() || aiTarget.SoundRange <= 0.0f) { continue; } if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) { DrawMarker(spriteBatch, - aiTarget.SonarLabel, + aiTarget.SonarLabel.Value, aiTarget.SonarIconIdentifier, aiTarget, aiTarget.WorldPosition, transducerCenter, @@ -937,7 +937,7 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.StartLocation.Name, - Level.Loaded.StartOutpost != null ? "outpost" : "location", + (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), Level.Loaded.StartLocation.Name, Level.Loaded.StartExitPosition, transducerCenter, displayScale, center, DisplayRadius); @@ -947,7 +947,7 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.EndLocation.Name, - Level.Loaded.EndOutpost != null ? "outpost" : "location", + (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), Level.Loaded.EndLocation.Name, Level.Loaded.EndExitPosition, transducerCenter, displayScale, center, DisplayRadius); @@ -958,8 +958,8 @@ namespace Barotrauma.Items.Components var cave = Level.Loaded.Caves[i]; if (!cave.DisplayOnSonar) { continue; } DrawMarker(spriteBatch, - caveLabel, - "cave", + caveLabel.Value, + "cave".ToIdentifier(), "cave" + i, cave.StartPos.ToVector2(), transducerCenter, displayScale, center, DisplayRadius); @@ -968,13 +968,13 @@ namespace Barotrauma.Items.Components int missionIndex = 0; foreach (Mission mission in GameMain.GameSession.Missions) { - if (!string.IsNullOrWhiteSpace(mission.SonarLabel)) + if (!mission.SonarLabel.IsNullOrWhiteSpace()) { int i = 0; foreach (Vector2 sonarPosition in mission.SonarPositions) { DrawMarker(spriteBatch, - mission.SonarLabel, + mission.SonarLabel.Value, mission.SonarIconIdentifier, "mission" + missionIndex + ":" + i, sonarPosition, transducerCenter, @@ -995,7 +995,7 @@ namespace Barotrauma.Items.Components var i = unobtainedMinerals.FirstOrDefault(); if (i == null) { continue; } DrawMarker(spriteBatch, - i.Name, "mineral", "mineralcluster" + i, + i.Name, "mineral".ToIdentifier(), "mineralcluster" + i, c.center, transducerCenter, displayScale, center, DisplayRadius * 0.95f, onlyShowTextOnMouseOver: true); @@ -1022,8 +1022,8 @@ namespace Barotrauma.Items.Components } DrawMarker(spriteBatch, - sub.Info.DisplayName, - sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", + sub.Info.DisplayName.Value, + (sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine").ToIdentifier(), sub, sub.WorldPosition, transducerCenter, displayScale, center, DisplayRadius * 0.95f); @@ -1611,7 +1611,7 @@ namespace Barotrauma.Items.Components sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f * blip.Alpha, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0); } - private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius, + private void DrawMarker(SpriteBatch spriteBatch, string label, Identifier iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius, bool onlyShowTextOnMouseOver = false) { float linearDist = Vector2.Distance(worldPosition, transducerPosition); @@ -1704,11 +1704,11 @@ namespace Barotrauma.Items.Components if (alpha <= 0.0f) { return; } - string wrappedLabel = ToolBox.WrapText(label, 150, GUI.SmallFont); + string wrappedLabel = ToolBox.WrapText(label, 150, GUIStyle.SmallFont.Value); wrappedLabel += "\n" + ((int)(dist * Physics.DisplayToRealWorldRatio) + " m"); Vector2 labelPos = markerPos; - Vector2 textSize = GUI.SmallFont.MeasureString(wrappedLabel); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(wrappedLabel); //flip the text to left side when the marker is on the left side or goes outside the right edge of the interface if (GuiFrame != null && (dir.X < 0.0f || labelPos.X + textSize.X + 10 > GuiFrame.Rect.X) && labelPos.X - textSize.X > 0) @@ -1720,7 +1720,7 @@ namespace Barotrauma.Items.Components new Vector2(labelPos.X + 10, labelPos.Y), wrappedLabel, Color.LightBlue * textAlpha * alpha, Color.Black * textAlpha * 0.8f * alpha, - 2, GUI.SmallFont); + 2, GUIStyle.SmallFont); } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index ee079c02a..2d2786db4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private bool dockingNetworkMessagePending; private GUIButton dockingButton; - private string dockText, undockText; + private LocalizedString dockText, undockText; private GUIComponent steerArea; @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components private GUITextBlock tipContainer; - private string noPowerTip, autoPilotMaintainPosTip, autoPilotLevelStartTip, autoPilotLevelEndTip; + private LocalizedString noPowerTip, autoPilotMaintainPosTip, autoPilotLevelStartTip, autoPilotLevelEndTip; private Sprite maintainPosIndicator, maintainPosOriginIndicator; private Sprite steeringIndicator; @@ -90,9 +90,9 @@ namespace Barotrauma.Items.Components } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -141,26 +141,26 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(steeringModeSwitch.RectTransform.RelativeSize.X, 0) }, style: null); manualPilotIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), steeringModeRightSide.RectTransform, Anchor.TopLeft), - TextManager.Get("SteeringManual"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SteeringManual"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { Selected = !autoPilot, Enabled = false }; autopilotIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), steeringModeRightSide.RectTransform, Anchor.BottomLeft), - TextManager.Get("SteeringAutoPilot"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SteeringAutoPilot"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { Selected = autoPilot, Enabled = false }; - manualPilotIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - autopilotIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + manualPilotIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + autopilotIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); GUITextBlock.AutoScaleAndNormalize(manualPilotIndicator.TextBlock, autopilotIndicator.TextBlock); var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame"); var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null); maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), - TextManager.Get("SteeringMaintainPos"), font: GUI.SmallFont, style: "GUIRadioButton") + TextManager.Get("SteeringMaintainPos"), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = maintainPos, @@ -194,10 +194,10 @@ namespace Barotrauma.Items.Components return true; } }; - int textLimit = (int)(MathHelper.Clamp(25 * GUI.xScale, 15, 35)); + int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), - GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, textLimit), - font: GUI.SmallFont, style: "GUIRadioButton") + GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit), + font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = levelStartSelected, @@ -223,8 +223,8 @@ namespace Barotrauma.Items.Components }; levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter), - (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, textLimit), - font: GUI.SmallFont, style: "GUIRadioButton") + (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, GUIStyle.SmallFont, textLimit), + font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = levelEndSelected, @@ -290,7 +290,7 @@ namespace Barotrauma.Items.Components leftElements.Add(left); centerElements.Add(center); rightElements.Add(right); - string leftText = string.Empty, centerText = string.Empty; + LocalizedString leftText = string.Empty, centerText = string.Empty; GUITextBlock.TextGetterHandler rightTextGetter = null; switch (i) { @@ -325,10 +325,10 @@ namespace Barotrauma.Items.Components }; break; } - new GUITextBlock(new RectTransform(Vector2.One, left.RectTransform), leftText, font: GUI.SubHeadingFont, wrap: leftText.Contains(' '), textAlignment: Alignment.CenterRight); - new GUITextBlock(new RectTransform(Vector2.One, center.RectTransform), centerText, font: GUI.Font, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(Vector2.One, left.RectTransform), leftText, font: GUIStyle.SubHeadingFont, wrap: leftText.Contains(" "), textAlignment: Alignment.CenterRight); + new GUITextBlock(new RectTransform(Vector2.One, center.RectTransform), centerText, font: GUIStyle.Font, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; var digitalFrame = new GUIFrame(new RectTransform(Vector2.One, right.RectTransform), style: "DigitalFrameDark"); - new GUITextBlock(new RectTransform(Vector2.One * 0.85f, digitalFrame.RectTransform, Anchor.Center), "12345", GUI.Style.TextColorDark, GUI.DigitalFont, Alignment.CenterRight) + new GUITextBlock(new RectTransform(Vector2.One * 0.85f, digitalFrame.RectTransform, Anchor.Center), "12345", GUIStyle.TextColorDark, GUIStyle.DigitalFont, Alignment.CenterRight) { TextGetter = rightTextGetter }; @@ -345,8 +345,8 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(Sonar.controlBoxOffset.X + 0.05f, -0.05f) }, style: null); - dockText = TextManager.Get("label.navterminaldock", fallBackTag: "captain.dock"); - undockText = TextManager.Get("label.navterminalundock", fallBackTag: "captain.undock"); + dockText = TextManager.Get("label.navterminaldock", "captain.dock"); + undockText = TextManager.Get("label.navterminalundock", "captain.undock"); dockingButton = new GUIButton(new RectTransform(new Vector2(elementScale), dockingContainer.RectTransform, Anchor.Center), dockText, style: "PowerButton") { OnClicked = (btn, userdata) => @@ -376,13 +376,13 @@ namespace Barotrauma.Items.Components enterOutpostPrompt = new GUIMessageBox( TextManager.GetWithVariable("enterlocation", "[locationname]", DockingTarget.Item.Submarine.Info.Name), TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); } else { enterOutpostPrompt = new GUIMessageBox("", TextManager.GetWithVariable("campaignenteroutpostprompt", "[locationname]", DockingTarget.Item.Submarine.Info.Name), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); } enterOutpostPrompt.Buttons[0].OnClicked += (btn, userdata) => { @@ -411,11 +411,11 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } } - dockingButton.Font = GUI.SubHeadingFont; + dockingButton.Font = GUIStyle.SubHeadingFont; dockingButton.TextBlock.RectTransform.MaxSize = new Point((int)(dockingButton.Rect.Width * 0.7f), int.MaxValue); dockingButton.TextBlock.AutoScaleHorizontal = true; - var style = GUI.Style.GetComponentStyle("DockingButtonUp"); + var style = GUIStyle.GetComponentStyle("DockingButtonUp"); Sprite buttonSprite = style.Sprites.FirstOrDefault().Value.FirstOrDefault()?.Sprite; Point buttonSize = buttonSprite != null ? buttonSprite.size.ToPoint() : new Point(149, 52); Point horizontalButtonSize = buttonSize.Multiply(elementScale * GUI.Scale * dockingButtonSize); @@ -447,18 +447,18 @@ namespace Barotrauma.Items.Components steerRadius = steerArea.Rect.Width / 2; iceSpireWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), - TextManager.Get("NavTerminalIceSpireWarning"), GUI.Style.Red, GUI.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f, wrap: true) + TextManager.Get("NavTerminalIceSpireWarning"), GUIStyle.Red, GUIStyle.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f, wrap: true) { Visible = false }; pressureWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), - TextManager.Get("SteeringDepthWarning"), GUI.Style.Red, GUI.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f) + TextManager.Get("SteeringDepthWarning"), GUIStyle.Red, GUIStyle.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f) { Visible = false }; // Tooltip/helper text tipContainer = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), steerArea.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) - , "", font: GUI.Font, wrap: true, style: "GUIToolTip", textAlignment: Alignment.Center) + , "", font: GUIStyle.Font, wrap: true, style: "GUIToolTip", textAlignment: Alignment.Center) { AutoScaleHorizontal = true }; @@ -524,7 +524,7 @@ namespace Barotrauma.Items.Components if (velRect.Contains(PlayerInput.MousePosition)) { - GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringInputPos.X - 4, (int)steeringInputPos.Y - 4, 8, 8), GUI.Style.Red, thickness: 2); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringInputPos.X - 4, (int)steeringInputPos.Y - 4, 8, 8), GUIStyle.Red, thickness: 2); } } else if (posToMaintain.HasValue && !LevelStartSelected && !LevelEndSelected) @@ -537,7 +537,7 @@ namespace Barotrauma.Items.Components displayPosToMaintain = displayPosToMaintain.ClampLength(velRect.Width / 2); displayPosToMaintain = steerArea.Rect.Center.ToVector2() + displayPosToMaintain; - Color crosshairColor = GUI.Style.Orange * (0.5f + ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 4.0f); + Color crosshairColor = GUIStyle.Orange * (0.5f + ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 4.0f); if (maintainPosIndicator != null) { maintainPosIndicator.Draw(spriteBatch, displayPosToMaintain, crosshairColor, scale: 0.5f * sonar.Zoom); @@ -551,11 +551,11 @@ namespace Barotrauma.Items.Components if (maintainPosOriginIndicator != null) { - maintainPosOriginIndicator.Draw(spriteBatch, steeringOrigin, GUI.Style.Orange, scale: 0.5f * sonar.Zoom); + maintainPosOriginIndicator.Draw(spriteBatch, steeringOrigin, GUIStyle.Orange, scale: 0.5f * sonar.Zoom); } else { - GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringOrigin.X - 5, (int)steeringOrigin.Y - 5, 10, 10), GUI.Style.Orange); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringOrigin.X - 5, (int)steeringOrigin.Y - 5, 10, 10), GUIStyle.Orange); } } } @@ -596,11 +596,11 @@ namespace Barotrauma.Items.Components pos.Y = -pos.Y; pos += center; - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 3 / 2, (int)pos.Y - 3, 6, 6), (SteeringPath.CurrentNode == wp) ? Color.LightGreen : GUI.Style.Green, false); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 3 / 2, (int)pos.Y - 3, 6, 6), (SteeringPath.CurrentNode == wp) ? Color.LightGreen : GUIStyle.Green, false); if (prevPos != Vector2.Zero) { - GUI.DrawLine(spriteBatch, pos, prevPos, GUI.Style.Green); + GUI.DrawLine(spriteBatch, pos, prevPos, GUIStyle.Green); } prevPos = pos; @@ -618,14 +618,14 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, pos1, pos2, - GUI.Style.Red * 0.6f, width: 3); + GUIStyle.Red * 0.6f, width: 3); if (obstacle.Intersection.HasValue) { Vector2 intersectionPos = (obstacle.Intersection.Value - transducerCenter) *displayScale; intersectionPos.Y = -intersectionPos.Y; intersectionPos += center; - GUI.DrawRectangle(spriteBatch, intersectionPos - Vector2.One * 2, Vector2.One * 4, GUI.Style.Red); + GUI.DrawRectangle(spriteBatch, intersectionPos - Vector2.One * 2, Vector2.One * 4, GUIStyle.Red); } Vector2 obstacleCenter = (pos1 + pos2) / 2; @@ -634,7 +634,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, obstacleCenter, obstacleCenter + new Vector2(obstacle.AvoidStrength.X, -obstacle.AvoidStrength.Y) * 100, - Color.Lerp(GUI.Style.Green, GUI.Style.Orange, obstacle.Dot), width: 2); + Color.Lerp(GUIStyle.Green, GUIStyle.Orange, obstacle.Dot), width: 2); } } } @@ -673,7 +673,7 @@ namespace Barotrauma.Items.Components dockingButton.Text = dockText; if (dockingButton.FlashTimer <= 0.0f) { - dockingButton.Flash(GUI.Style.Blue, 0.5f, useCircularFlash: true); + dockingButton.Flash(GUIStyle.Blue, 0.5f, useCircularFlash: true); dockingButton.Pulsate(Vector2.One, Vector2.One * 1.2f, dockingButton.FlashTimer); } } @@ -689,7 +689,7 @@ namespace Barotrauma.Items.Components statusContainer.Visible = false; if (dockingButton.FlashTimer <= 0.0f) { - dockingButton.Flash(GUI.Style.Orange, useCircularFlash: true); + dockingButton.Flash(GUIStyle.Orange, useCircularFlash: true); dockingButton.Pulsate(Vector2.One, Vector2.One * 1.2f, dockingButton.FlashTimer); } } @@ -733,7 +733,10 @@ namespace Barotrauma.Items.Components item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - depthEffectThreshold && item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - depthEffectThreshold) { pressureWarningText.Visible = true; - pressureWarningText.Text = item.Submarine.AtDamageDepth ? TextManager.Get("SteeringDepthWarning") : TextManager.Get("SteeringDepthWarningLow").Replace("[crushdepth]", ((int)item.Submarine.RealWorldCrushDepth).ToString()); + pressureWarningText.Text = + item.Submarine.AtDamageDepth ? + TextManager.Get("SteeringDepthWarning") : + TextManager.GetWithVariable("SteeringDepthWarningLow", "[crushdepth]", ((int)item.Submarine.RealWorldCrushDepth).ToString()); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index f8962c80e..2a011d6df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -10,10 +10,10 @@ namespace Barotrauma.Items.Components private GUIProgressBar chargeIndicator; private GUIScrollBar rechargeSpeedSlider; - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float RechargeWarningIndicatorLow { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float RechargeWarningIndicatorHigh { get; set; } public Vector2 DrawSize @@ -36,17 +36,17 @@ namespace Barotrauma.Items.Components var rechargeRateContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), upperArea.RectTransform), style: null); var rechargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), rechargeRateContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get("rechargerate"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); - string kW = TextManager.Get("kilowatt"); + TextManager.Get("rechargerate"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + LocalizedString kW = TextManager.Get("kilowatt"); var rechargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), rechargeRateContainer.RectTransform, Anchor.CenterRight), - "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) + "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight) { TextGetter = () => $"{(int)MathF.Round(currPowerConsumption)} {kW} ({(int)MathF.Round(RechargeRatio * 100)} %)" }; - if (rechargeText.TextSize.X > rechargeText.Rect.Width) { rechargeText.Font = GUI.SmallFont; } + if (rechargeText.TextSize.X > rechargeText.Rect.Width) { rechargeText.Font = GUIStyle.SmallFont; } var rechargeSliderContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.4f), upperArea.RectTransform, Anchor.BottomCenter)); - + if (RechargeWarningIndicatorLow > 0.0f || RechargeWarningIndicatorHigh > 0.0f) { var rechargeSliderFill = new GUICustomComponent(new RectTransform(new Vector2(0.95f, 0.9f), rechargeSliderContainer.RectTransform, Anchor.Center), (SpriteBatch sb, GUICustomComponent c) => @@ -54,12 +54,12 @@ namespace Barotrauma.Items.Components if (RechargeWarningIndicatorLow > 0.0f) { float warningLow = c.Rect.Width * RechargeWarningIndicatorLow; - GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningLow, c.Rect.Y), new Vector2(c.Rect.Width - warningLow, c.Rect.Height), GUI.Style.Orange, isFilled: true); + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningLow, c.Rect.Y), new Vector2(c.Rect.Width - warningLow, c.Rect.Height), GUIStyle.Orange, isFilled: true); } if (RechargeWarningIndicatorHigh > 0.0f) { float warningHigh = c.Rect.Width * RechargeWarningIndicatorHigh; - GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningHigh, c.Rect.Y), new Vector2(c.Rect.Width - warningHigh, c.Rect.Height), GUI.Style.Red, isFilled: true); + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningHigh, c.Rect.Y), new Vector2(c.Rect.Width - warningHigh, c.Rect.Height), GUIStyle.Red, isFilled: true); } }); } @@ -88,17 +88,17 @@ namespace Barotrauma.Items.Components var chargeTextContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), lowerArea.RectTransform), style: null); var chargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), chargeTextContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get("charge"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + TextManager.Get("charge"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("PowerTransferTipPower") }; - string kWmin = TextManager.Get("kilowattminute"); + LocalizedString kWmin = TextManager.Get("kilowattminute"); var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), chargeTextContainer.RectTransform, Anchor.CenterRight), - "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) + "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight) { TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)capacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, capacity))} %)" }; - if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUI.SmallFont; } + if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUIStyle.SmallFont; } chargeIndicator = new GUIProgressBar(new RectTransform(new Vector2(1.1f, 0.5f), lowerArea.RectTransform, Anchor.BottomCenter) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs index 809f7b497..0a1cbd605 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs @@ -25,25 +25,25 @@ namespace Barotrauma.Items.Components Stretch = true }; powerIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), - TextManager.Get("PowerTransferPowered"), font: GUI.SubHeadingFont, style: "IndicatorLightGreen") + TextManager.Get("PowerTransferPowered"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightGreen") { CanBeFocused = false }; highVoltageIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), - TextManager.Get("PowerTransferHighVoltage"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("PowerTransferHighVoltage"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { ToolTip = TextManager.Get("PowerTransferTipOvervoltage"), Enabled = false }; lowVoltageIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), - TextManager.Get("PowerTransferLowVoltage"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("PowerTransferLowVoltage"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { ToolTip = TextManager.Get("PowerTransferTipLowvoltage"), Enabled = false }; - powerIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - highVoltageIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - lowVoltageIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + highVoltageIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + lowVoltageIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, highVoltageIndicator.TextBlock, lowVoltageIndicator.TextBlock); var textContainer = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), paddedFrame.RectTransform, Anchor.CenterRight), style: null); @@ -57,26 +57,33 @@ namespace Barotrauma.Items.Components }; var powerLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), upperTextArea.RectTransform), - TextManager.Get("PowerTransferPowerLabel"), textColor: GUI.Style.TextColorBright, font: GUI.LargeFont, textAlignment: Alignment.CenterRight) + TextManager.Get("PowerTransferPowerLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipPower") }; var loadLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), lowerTextArea.RectTransform), - TextManager.Get("PowerTransferLoadLabel"), textColor: GUI.Style.TextColorBright, font: GUI.LargeFont, textAlignment: Alignment.CenterRight) + TextManager.Get("PowerTransferLoadLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipLoad") }; var digitalBackground = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.8f), upperTextArea.RectTransform), style: "DigitalFrameDark"); var powerText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.95f), digitalBackground.RectTransform, Anchor.Center), - "", font: GUI.DigitalFont, textColor: GUI.Style.TextColorDark) + "", font: GUIStyle.DigitalFont, textColor: GUIStyle.TextColorDark) { TextAlignment = Alignment.CenterRight, ToolTip = TextManager.Get("PowerTransferTipPower"), - TextGetter = () => ((int)Math.Round(-currPowerConsumption)).ToString() + TextGetter = () => { + float currPower = powerLoad < 0 ? -powerLoad: 0; + if (!(this is RelayComponent) && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null) + { + currPower = PowerConnections[0].Grid.Power; + } + return ((int)Math.Round(currPower)).ToString(); + } }; var kw1 = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.5f), upperTextArea.RectTransform), - TextManager.Get("kilowatt"), textColor: GUI.Style.TextColor, font: GUI.Font) + TextManager.Get("kilowatt"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font) { Padding = Vector4.Zero, TextAlignment = Alignment.BottomCenter @@ -84,14 +91,26 @@ namespace Barotrauma.Items.Components digitalBackground = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.8f), lowerTextArea.RectTransform), style: "DigitalFrameDark"); var loadText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.95f), digitalBackground.RectTransform, Anchor.Center), - "", font: GUI.DigitalFont, textColor: GUI.Style.TextColorDark) + "", font: GUIStyle.DigitalFont, textColor: GUIStyle.TextColorDark) { TextAlignment = Alignment.CenterRight, ToolTip = TextManager.Get("PowerTransferTipLoad"), - TextGetter = () => ((int)Math.Round(this is RelayComponent relay ? relay.DisplayLoad : powerLoad)).ToString() + TextGetter = () => + { + float load = PowerLoad; + if (this is RelayComponent relay) + { + load = relay.DisplayLoad; + } + else if (load < 0) + { + load = 0; + } + return ((int)Math.Round(load)).ToString(); + } }; var kw2 = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.5f), lowerTextArea.RectTransform), - TextManager.Get("kilowatt"), textColor: GUI.Style.TextColor, font: GUI.Font) + TextManager.Get("kilowatt"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font) { Padding = Vector4.Zero, TextAlignment = Alignment.BottomCenter @@ -106,8 +125,8 @@ namespace Barotrauma.Items.Components { if (GuiFrame == null) return; - float voltage = powerLoad <= 0.0f ? 1.0f : -currPowerConsumption / powerLoad; - powerIndicator.Selected = IsActive && currPowerConsumption < -0.1f; + float voltage = (PowerConnections.Count > 0 && PowerConnections[0].Grid != null) ? PowerConnections[0].Grid.Voltage : 0f; + powerIndicator.Selected = IsActive && voltage > 0; highVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage > 1.2f; lowVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage < 0.8f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs index acf4b27c2..e3d4e35c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs @@ -9,14 +9,14 @@ namespace Barotrauma.Items.Components private RoundSound powerOnSound; private bool powerOnSoundPlayed; - partial void InitProjectSpecific(XElement element) + partial void InitProjectSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "poweronsound": - powerOnSound = Submarine.LoadRoundSound(subElement, false); + powerOnSound = RoundSound.Load(subElement, false); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 6fce23838..bd467a7b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -115,9 +115,9 @@ namespace Barotrauma.Items.Components } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs index 8c17e9d90..deeac3cbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs @@ -7,14 +7,14 @@ namespace Barotrauma.Items.Components { partial class Quality : ItemComponent { - public override void AddTooltipInfo(ref string name, ref string description) + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { foreach (var statValue in statValues) { int roundedValue = (int)Math.Round(statValue.Value * qualityLevel * 100); if (roundedValue == 0) { return; } - string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); - description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}%‖color:end‖ {TextManager.Get("qualitystattypenames." + statValue.Key.ToString(), true) ?? statValue.Key.ToString()}"; + string colorStr = XMLExtensions.ColorToString(GUIStyle.Green); + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}%‖color:end‖ {TextManager.Get("qualitystattypenames." + statValue.Key.ToString()).Fallback(statValue.Key.ToString())}"; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 93af5d8f2..759f67ad6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -31,9 +31,9 @@ namespace Barotrauma.Items.Components private float prevProgressBarState; private Item prevProgressBarTarget = null; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -41,10 +41,10 @@ namespace Barotrauma.Items.Components particleEmitters.Add(new ParticleEmitter(subElement)); break; case "particleemitterhititem": - string[] identifiers = subElement.GetAttributeStringArray("identifiers", new string[0]); - if (identifiers.Length == 0) identifiers = subElement.GetAttributeStringArray("identifier", new string[0]); - string[] excludedIdentifiers = subElement.GetAttributeStringArray("excludedidentifiers", new string[0]); - if (excludedIdentifiers.Length == 0) excludedIdentifiers = subElement.GetAttributeStringArray("excludedidentifier", new string[0]); + Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifiers", Array.Empty()); + if (identifiers.Length == 0) { identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); } + Identifier[] excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifiers", Array.Empty()); + if (excludedIdentifiers.Length == 0) { excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifier", Array.Empty()); } particleEmitterHitItem.Add( new Pair( @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components targetStructure.ID * 1000 + sectionIndex, //unique "identifier" for each wall section progressBarPos, MathUtils.InverseLerp(targetStructure.Prefab.MinHealth, targetStructure.Health, targetStructure.Health - targetStructure.SectionDamage(sectionIndex)), - GUI.Style.Red, GUI.Style.Green); + GUIStyle.Red, GUIStyle.Green); if (progressBar != null) progressBar.Size = new Vector2(60.0f, 20.0f); @@ -128,7 +128,7 @@ namespace Barotrauma.Items.Components targetItem, progressBarPos, progressBarState, - GUI.Style.Red, GUI.Style.Green, + GUIStyle.Red, GUIStyle.Green, progressBarState < prevProgressBarState ? "progressbar.cutting" : ""); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 0900f4fed..1a1497aba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -29,9 +29,9 @@ namespace Barotrauma.Items.Components private SoundChannel repairSoundChannel; - private string repairButtonText, repairingText; - private string sabotageButtonText, sabotagingText; - private string tinkerButtonText, tinkeringText; + private LocalizedString repairButtonText, repairingText; + private LocalizedString sabotageButtonText, sabotagingText; + private LocalizedString tinkerButtonText, tinkeringText; private FixActions requestStartFixAction; @@ -44,7 +44,7 @@ namespace Barotrauma.Items.Components public float FakeBrokenTimer; - [Serialize("", false, description: "An optional description of the needed repairs displayed in the repair interface.")] + [Serialize("", IsPropertySaveable.No, description: "An optional description of the needed repairs displayed in the repair interface.")] public string Description { get; @@ -78,10 +78,10 @@ namespace Barotrauma.Items.Components return false; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { CreateGUI(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -124,18 +124,18 @@ namespace Barotrauma.Items.Components }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), - header, textAlignment: Alignment.TopCenter, font: GUI.LargeFont); + header, textAlignment: Alignment.TopCenter, font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - Description, font: GUI.SmallFont, wrap: true); + Description, font: GUIStyle.SmallFont, wrap: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - TextManager.Get("RequiredRepairSkills"), font: GUI.SubHeadingFont); + TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont); for (int i = 0; i < requiredSkills.Count; i++) { var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), - font: GUI.SmallFont) + font: GUIStyle.SmallFont) { UserData = requiredSkills[i] }; @@ -148,9 +148,9 @@ namespace Barotrauma.Items.Components }; progressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), progressBarHolder.RectTransform), - color: GUI.Style.Green, barSize: 0.0f, style: "DeviceProgressBar"); + color: GUIStyle.Green, barSize: 0.0f, style: "DeviceProgressBar"); - progressBarOverlayText = new GUITextBlock(new RectTransform(Vector2.One, progressBar.RectTransform), string.Empty, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) + progressBarOverlayText = new GUITextBlock(new RectTransform(Vector2.One, progressBar.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) { IgnoreLayoutGroups = true }; @@ -203,8 +203,8 @@ namespace Barotrauma.Items.Components } }; - tinkerButtonText = TextManager.Get("TinkerButton", returnNull: true) ?? "Tinker"; - tinkeringText = TextManager.Get("Tinkering", returnNull: true) ?? "Tinkering"; + tinkerButtonText = TextManager.Get("TinkerButton").Fallback("Tinker"); + tinkeringText = TextManager.Get("Tinkering").Fallback("Tinkering"); TinkerButton = new GUIButton(new RectTransform(Vector2.One, extraButtonContainer.RectTransform), tinkerButtonText, style: "GUIButtonSmall") { IgnoreLayoutGroups = true, @@ -295,24 +295,24 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { IsActive = true; - + float defaultMaxCondition = (item.MaxCondition / item.MaxRepairConditionMultiplier); progressBar.BarSize = item.Condition / defaultMaxCondition; - progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green); + progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green); Rectangle sliderRect = progressBar.GetSliderRect(1.0f); Color qteSliderColor = Color.White; if (qteCooldown > 0.0f) { - qteSliderColor = qteSuccess ? GUI.Style.Green : GUI.Style.Red * 0.5f; + qteSliderColor = qteSuccess ? GUIStyle.Green : GUIStyle.Red * 0.5f; progressBar.Color = ToolBox.GradientLerp(qteCooldown / QteCooldownDuration, progressBar.Color, qteSliderColor, Color.White); } else { if (qteTimer / QteDuration <= item.Condition / item.MaxCondition) { - qteSliderColor = Color.Lerp(qteSliderColor, GUI.Style.Green, 0.5f); + qteSliderColor = Color.Lerp(qteSliderColor, GUIStyle.Green, 0.5f); } } @@ -324,7 +324,7 @@ namespace Barotrauma.Items.Components if (item.Condition > defaultMaxCondition) { float extraCondition = item.MaxCondition * (item.MaxRepairConditionMultiplier - 1.0f); - progressBar.Color = ToolBox.GradientLerp((item.Condition - defaultMaxCondition) / extraCondition, GUI.Style.ColorReputationHigh, GUI.Style.ColorReputationVeryHigh); + progressBar.Color = ToolBox.GradientLerp((item.Condition - defaultMaxCondition) / extraCondition, GUIStyle.ColorReputationHigh, GUIStyle.ColorReputationVeryHigh); progressBarOverlayText.Visible = true; progressBarOverlayText.Text = $"{(int)Math.Round((item.Condition / defaultMaxCondition) * 100)}%"; } @@ -364,7 +364,7 @@ namespace Barotrauma.Items.Components GUITextBlock textBlock = (GUITextBlock)c; if (character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier)) { - textBlock.TextColor = GUI.Style.Red; + textBlock.TextColor = GUIStyle.Red; } else { @@ -388,11 +388,11 @@ namespace Barotrauma.Items.Components { GUI.DrawString(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Deteriorating at " + (int)(DeteriorationSpeed * 60.0f) + " units/min" + (paused ? " [PAUSED]" : ""), - paused ? Color.Cyan : GUI.Style.Red, Color.Black * 0.5f); + paused ? Color.Cyan : GUIStyle.Red, Color.Black * 0.5f); } GUI.DrawString(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition, - GUI.Style.Orange); + GUIStyle.Orange); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index e98bf3f58..783864f76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -10,28 +10,28 @@ namespace Barotrauma.Items.Components { private Sprite sprite, startSprite, endSprite; - [Serialize(5, false)] + [Serialize(5, IsPropertySaveable.No)] public int SpriteWidth { get; set; } - [Serialize("255,255,255,255", false)] + [Serialize("255,255,255,255", IsPropertySaveable.No)] public Color SpriteColor { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool Tile { get; set; } - [Serialize("0.5,0.5)", false)] + [Serialize("0.5,0.5)", IsPropertySaveable.No)] public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); public Vector2 DrawSize @@ -62,9 +62,9 @@ namespace Barotrauma.Items.Components return sourcePos; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs index a44dca68e..865f0915d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components Character.Controlled?.UpdateHUDProgressBar(this, item.WorldPosition, ScanTimer / ScanDuration, - GUI.Style.Red, GUI.Style.Green, + GUIStyle.Red, GUIStyle.Green, textTag: "progressbar.scanning"); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index d0a181824..2202530c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components private GUIImage containerIndicator; private GUIComponentStyle indicatorStyleRed, indicatorStyleGreen; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { terminalButtonStyles = new string[RequiredSignalCount]; int i = 0; @@ -24,8 +24,8 @@ namespace Barotrauma.Items.Components if (style == null) { continue; } terminalButtonStyles[i++] = style; } - indicatorStyleRed = GUI.Style.GetComponentStyle("IndicatorLightRed"); - indicatorStyleGreen = GUI.Style.GetComponentStyle("IndicatorLightGreen"); + indicatorStyleRed = GUIStyle.GetComponentStyle("IndicatorLightRed"); + indicatorStyleGreen = GUIStyle.GetComponentStyle("IndicatorLightGreen"); CreateGUI(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 2e8e07a2b..cfe0fa95b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -190,7 +190,7 @@ namespace Barotrauma.Items.Components if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } Connection recipient = wire.OtherConnection(null); - string label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; + LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } DrawWire(spriteBatch, wire, new Vector2(x, y + height - 100 * GUI.Scale), new Vector2(x, y + height), @@ -208,17 +208,17 @@ namespace Barotrauma.Items.Components private void DrawConnection(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 labelPos, Vector2 scale) { - string text = DisplayName.ToUpper(); + string text = DisplayName.Value.ToUpper(); //nasty - if (GUI.Style.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First() is UISprite labelSprite) + if (GUIStyle.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First() is UISprite labelSprite) { Rectangle labelArea = GetLabelArea(labelPos, text, scale); - labelSprite.Draw(spriteBatch, labelArea, IsPower ? GUI.Style.Red : Color.SteelBlue); + labelSprite.Draw(spriteBatch, labelArea, IsPower ? GUIStyle.Red : Color.SteelBlue); } - GUI.DrawString(spriteBatch, labelPos + Vector2.UnitY, text, Color.Black * 0.8f, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, labelPos, text, GUI.Style.TextColorBright, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, labelPos + Vector2.UnitY, text, Color.Black * 0.8f, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont); float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale); @@ -234,7 +234,7 @@ namespace Barotrauma.Items.Components if (wires[i].HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } Connection recipient = wires[i].OtherConnection(this); - string label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; + LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; if (wires[i].Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } DrawWire(spriteBatch, wires[i], position, wirePosition, equippedWire, panel, label); @@ -295,7 +295,7 @@ namespace Barotrauma.Items.Components { FlashTimer = flashDuration; this.flashDuration = flashDuration; - flashColor = (color == null) ? GUI.Style.Red : (Color)color; + flashColor = (color == null) ? GUIStyle.Red : (Color)color; } public void UpdateFlashTimer(float deltaTime) @@ -304,7 +304,7 @@ namespace Barotrauma.Items.Components FlashTimer -= deltaTime; } - private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, string label) + private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, LocalizedString label) { int textX = (int)start.X; if (start.X < end.X) @@ -324,20 +324,20 @@ namespace Barotrauma.Items.Components Vector2.Distance(end, PlayerInput.MousePosition) < 20.0f || new Rectangle((start.X < end.X) ? textX - 100 : textX, (int)start.Y - 5, 100, 14).Contains(PlayerInput.MousePosition)); - if (!string.IsNullOrEmpty(label)) + if (!label.IsNullOrEmpty()) { if (start.Y > panel.GuiFrame.Rect.Bottom - 1.0f) { //wire at the bottom of the panel -> draw the text below the panel, tilted 45 degrees - GUI.Font.DrawString(spriteBatch, label, start + Vector2.UnitY * 20 * GUI.Scale, Color.White, 45.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0.0f); + GUIStyle.Font.DrawString(spriteBatch, label, start + Vector2.UnitY * 20 * GUI.Scale, Color.White, 45.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0.0f); } else { GUI.DrawString(spriteBatch, - new Vector2(start.X < end.X ? textX - GUI.SmallFont.MeasureString(label).X : textX, start.Y - 5.0f), + new Vector2(start.X < end.X ? textX - GUIStyle.SmallFont.MeasureString(label).X : textX, start.Y - 5.0f), label, - wire.Locked ? GUI.Style.TextColorDim : (mouseOn ? Wire.higlightColor : GUI.Style.TextColor), Color.Black * 0.9f, - 3, GUI.SmallFont); + wire.Locked ? GUIStyle.TextColorDim : (mouseOn ? Wire.higlightColor : GUIStyle.TextColorNormal), Color.Black * 0.9f, + 3, GUIStyle.SmallFont); } } @@ -401,13 +401,13 @@ namespace Barotrauma.Items.Components { if (c.IsOutput) { - var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.ToUpper(), scale); + var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.Value.ToUpper(), scale); labelAreas.Add(labelArea); rightPos.Y += connectorIntervalLeft; } else { - var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.ToUpper(), scale); + var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.Value.ToUpper(), scale); labelAreas.Add(labelArea); leftPos.Y += connectorIntervalRight; } @@ -455,19 +455,19 @@ namespace Barotrauma.Items.Components { return new Vector2( connectorPosition.X + 25 * panel.Scale, - connectorPosition.Y - 5 * panel.Scale - GUI.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y); + connectorPosition.Y - 5 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y); } private static Vector2 GetOutputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) { return new Vector2( - connectorPosition.X - 25 * panel.Scale - GUI.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, + connectorPosition.X - 25 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, connectorPosition.Y + 5 * panel.Scale); } private static Rectangle GetLabelArea(Vector2 labelPos, string text, Vector2 scale) { - Vector2 textSize = GUI.SmallFont.MeasureString(text); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Rectangle labelArea = new Rectangle(labelPos.ToPoint(), textSize.ToPoint()); labelArea.Inflate(10 * scale.X, 3 * scale.Y); return labelArea; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 72e8482da..057679a25 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components HighlightedWire = null; Connection.DrawConnections(spriteBatch, this, user); - foreach (UISprite sprite in GUI.Style.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None]) + foreach (UISprite sprite in GUIStyle.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None]) { sprite.Draw(spriteBatch, GuiFrame.Rect, Color.White, SpriteEffects.None); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 5a24346cc..f22fbcce1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -44,7 +44,7 @@ namespace Barotrauma.Items.Components UserData = ciElement }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), - TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label); + TextManager.Get(ciElement.Label).Fallback(ciElement.Label)); if (!ciElement.IsIntegerInput) { var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon") @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform) { MaxSize = ElementMaxSize - }, TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label) + }, TextManager.Get(ciElement.Label).Fallback(ciElement.Label)) { UserData = ciElement }; @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components else { var btn = new GUIButton(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), - TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label, style: "DeviceButton") + TextManager.Get(ciElement.Label).Fallback(ciElement.Label), style: "DeviceButton") { UserData = ciElement }; @@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components } } - string CreateLabelText(int elementIndex) + LocalizedString CreateLabelText(int elementIndex) { return string.IsNullOrWhiteSpace(customInterfaceElementList[elementIndex].Label) ? TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index f9c20a0ff..7538b6fc4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -90,7 +90,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), "> " + input, - textColor: color, wrap: true, font: UseMonospaceFont ? GUI.MonospacedFont : GUI.GlobalFont) + textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.GlobalFont) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index f4b055a39..4e1de585c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components if (width <= 0f) { return; } RecalculateVertices(wire, width); - for (int i=0;i draggingWire; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { if (defaultWireSprite == null) { @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components }; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase)) { @@ -286,7 +286,7 @@ namespace Barotrauma.Items.Components WireSection.Draw( spriteBatch, this, start, endPos, - GUI.Style.Orange, depth + 0.00001f, 0.2f); + GUIStyle.Orange, depth + 0.00001f, 0.2f); WireSection.Draw( spriteBatch, this, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 88367f1a0..e956c1721 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { partial class StatusHUD : ItemComponent { - private static readonly string[] BleedingTexts = + private static readonly LocalizedString[] BleedingTexts = { TextManager.Get("MinorBleeding"), TextManager.Get("Bleeding"), @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components TextManager.Get("CatastrophicBleeding") }; - private static readonly string[] OxygenTexts = + private static readonly LocalizedString[] OxygenTexts = { TextManager.Get("OxygenNormal"), TextManager.Get("OxygenReduced"), @@ -25,42 +25,42 @@ namespace Barotrauma.Items.Components TextManager.Get("NotBreathing") }; - [Serialize(500.0f, false, description: "How close to a target the user must be to see their health data (in pixels).")] + [Serialize(500.0f, IsPropertySaveable.No, description: "How close to a target the user must be to see their health data (in pixels).")] public float Range { get; private set; } - [Serialize(50.0f, false, description: "The range within which the health info texts fades out.")] + [Serialize(50.0f, IsPropertySaveable.No, description: "The range within which the health info texts fades out.")] public float FadeOutRange { get; private set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool ThermalGoggles { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool ShowDeadCharacters { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool ShowTexts { get; private set; } - [Serialize("72,119,72,120", false)] + [Serialize("72,119,72,120", IsPropertySaveable.No)] public Color OverlayColor { get; @@ -159,7 +159,8 @@ namespace Barotrauma.Items.Components if (OverlayColor.A > 0) { - GUI.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), OverlayColor); + GUIStyle.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), + OverlayColor); } if (ShowTexts) @@ -208,7 +209,7 @@ namespace Barotrauma.Items.Components float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist > Range * Range) { continue; } - Sprite pingCircle = GUI.Style.UIThermalGlow.Sprite; + Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; foreach (Limb limb in c.AnimController.Limbs) { if (limb.Mass < 1.0f) { continue; } @@ -230,71 +231,71 @@ namespace Barotrauma.Items.Components Vector2 hudPos = GameMain.GameScreen.Cam.WorldToScreen(target.DrawPosition); hudPos += Vector2.UnitX * 50.0f; - List texts = new List(); + List texts = new List(); List textColors = new List(); texts.Add(target.Info == null ? target.DisplayName : target.Info.DisplayName); - Color nameColor = GUI.Style.TextColor; + Color nameColor = GUIStyle.TextColorNormal; if (Character.Controlled != null && target.TeamID != Character.Controlled.TeamID) { - nameColor = target.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + nameColor = target.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUIStyle.Red; } textColors.Add(nameColor); if (target.IsDead) { texts.Add(TextManager.Get("Deceased")); - textColors.Add(GUI.Style.Red); + textColors.Add(GUIStyle.Red); if (target.CauseOfDeath != null) { texts.Add( target.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + target.CauseOfDeath.Type.ToString()))); - textColors.Add(GUI.Style.Red); + textColors.Add(GUIStyle.Red); } } else { - if (!string.IsNullOrEmpty(target.customInteractHUDText) && target.AllowCustomInteract) + if (!target.CustomInteractHUDText.IsNullOrEmpty() && target.AllowCustomInteract) { - texts.Add(target.customInteractHUDText); - textColors.Add(GUI.Style.Green); + texts.Add(target.CustomInteractHUDText); + textColors.Add(GUIStyle.Green); } if (!target.IsIncapacitated && target.IsPet) { - texts.Add(CharacterHUD.GetCachedHudText("PlayHint", GameMain.Config.KeyBindText(InputType.Use))); - textColors.Add(GUI.Style.Green); + texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); + textColors.Add(GUIStyle.Green); } if (target.CharacterHealth.UseHealthWindow && !target.DisableHealthWindow && equipper?.FocusedCharacter == target && equipper.CanInteractWith(target, 160f, false)) { - texts.Add(CharacterHUD.GetCachedHudText("HealHint", GameMain.Config.KeyBindText(InputType.Health))); - textColors.Add(GUI.Style.Green); + texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); + textColors.Add(GUIStyle.Green); } if (target.CanBeDragged) { - texts.Add(CharacterHUD.GetCachedHudText("GrabHint", GameMain.Config.KeyBindText(InputType.Grab))); - textColors.Add(GUI.Style.Green); + texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); + textColors.Add(GUIStyle.Green); } if (target.IsUnconscious) { texts.Add(TextManager.Get("Unconscious")); - textColors.Add(GUI.Style.Orange); + textColors.Add(GUIStyle.Orange); } if (target.Stun > 0.01f) { texts.Add(TextManager.Get("Stunned")); - textColors.Add(GUI.Style.Orange); + textColors.Add(GUIStyle.Orange); } int oxygenTextIndex = MathHelper.Clamp((int)Math.Floor((1.0f - (target.Oxygen / 100.0f)) * OxygenTexts.Length), 0, OxygenTexts.Length - 1); texts.Add(OxygenTexts[oxygenTextIndex]); - textColors.Add(Color.Lerp(GUI.Style.Red, GUI.Style.Green, target.Oxygen / 100.0f)); + textColors.Add(Color.Lerp(GUIStyle.Red, GUIStyle.Green, target.Oxygen / 100.0f)); if (target.Bleeding > 0.0f) { int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f) * BleedingTexts.Length, 0, BleedingTexts.Length - 1); texts.Add(BleedingTexts[bleedingTextIndex]); - textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, target.Bleeding / 100.0f)); + textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, target.Bleeding / 100.0f)); } var allAfflictions = target.CharacterHealth.GetAllAfflictions(); @@ -315,21 +316,21 @@ namespace Barotrauma.Items.Components foreach (AfflictionPrefab affliction in combinedAfflictionStrengths.Keys) { texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max((int)combinedAfflictionStrengths[affliction], 1).ToString() + " %")); - textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); + textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); } } - GUI.DrawString(spriteBatch, hudPos, texts[0], textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, hudPos, texts[0], textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SubHeadingFont); hudPos.X += 5.0f; - hudPos.Y += 24.0f; + hudPos.Y += 24.0f * GameSettings.CurrentConfig.Graphics.TextScale; hudPos.X = (int)hudPos.X; hudPos.Y = (int)hudPos.Y; for (int i = 1; i < texts.Count; i++) { - GUI.DrawString(spriteBatch, hudPos, texts[i], textColors[i] * alpha, Color.Black * 0.7f * alpha, 2, GUI.SmallFont); - hudPos.Y += 18.0f; + GUI.DrawString(spriteBatch, hudPos, texts[i], textColors[i] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SmallFont); + hudPos.Y += (int)(18.0f * GameSettings.CurrentConfig.Graphics.TextScale); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 98f9490aa..ded8496e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -57,42 +57,42 @@ namespace Barotrauma.Items.Components private readonly List particleEmitters = new List(); private readonly List particleEmitterCharges = new List(); - [Editable, Serialize("0,0,0,0", true, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] + [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] public Color HudTint { get; private set; } - [Serialize(false, false, description: "Should the charge of the connected batteries/supercapacitors be shown at the top of the screen when operating the item.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the charge of the connected batteries/supercapacitors be shown at the top of the screen when operating the item.")] public bool ShowChargeIndicator { get; private set; } - [Serialize(false, false, description: "Should the available ammunition be shown at the top of the screen when operating the item.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the available ammunition be shown at the top of the screen when operating the item.")] public bool ShowProjectileIndicator { get; private set; } - [Serialize(0.0f, false, description: "How far the barrel \"recoils back\" when the turret is fired (in pixels).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How far the barrel \"recoils back\" when the turret is fired (in pixels).")] public float RecoilDistance { get; private set; } - [Serialize(0.0f, false, description: "The distance in which the spinning barrels rotate. Only used if spinning barrels are created.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The distance in which the spinning barrels rotate. Only used if spinning barrels are created.")] public float SpinningBarrelDistance { get; private set; } - [Serialize(false, false, description: "Use firing offset for muzzleflash? This field shouldn't be needed but I'm using it for prototyping")] + [Serialize(false, IsPropertySaveable.No, description: "Use firing offset for muzzleflash? This field shouldn't be needed but I'm using it for prototyping")] public bool UseFiringOffsetForMuzzleFlash { get; @@ -125,33 +125,33 @@ namespace Barotrauma.Items.Components get { return barrelSprite; } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - string texturePath = subElement.GetAttributeString("texture", ""); + string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) { case "crosshair": - crosshairSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairSprite = new Sprite(subElement, path: textureDir); break; case "weaponindicator": - WeaponIndicatorSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + WeaponIndicatorSprite = new Sprite(subElement, path: textureDir); break; case "crosshairpointer": - crosshairPointerSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairPointerSprite = new Sprite(subElement, path: textureDir); break; case "startmovesound": - startMoveSound = Submarine.LoadRoundSound(subElement, false); + startMoveSound = RoundSound.Load(subElement, false); break; case "endmovesound": - endMoveSound = Submarine.LoadRoundSound(subElement, false); + endMoveSound = RoundSound.Load(subElement, false); break; case "movesound": - moveSound = Submarine.LoadRoundSound(subElement, false); + moveSound = RoundSound.Load(subElement, false); break; case "chargesound": - chargeSound = Submarine.LoadRoundSound(subElement, false); + chargeSound = RoundSound.Load(subElement, false); break; case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); @@ -437,15 +437,15 @@ namespace Barotrauma.Items.Components if (Math.Abs(minRotation - maxRotation) < 0.02f) { - spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUI.Style.Green, thickness: lineThickness); + spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUIStyle.Green, thickness: lineThickness); } else if (radians > Math.PI * 2) { - spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUI.Style.Red, thickness: lineThickness); + spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUIStyle.Red, thickness: lineThickness); } else { - spriteBatch.DrawSector(drawPos, circleRadius, radians, (int)Math.Abs(90 * radians), GUI.Style.Green, offset: minRotation, thickness: lineThickness); + spriteBatch.DrawSector(drawPos, circleRadius, radians, (int)Math.Abs(90 * radians), GUIStyle.Green, offset: minRotation, thickness: lineThickness); } int baseWidgetScale = GUI.IntScale(16); @@ -459,7 +459,7 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUI.Style.Green; + widget.color = GUIStyle.Green; prevAngle = minRotation; }; widget.Deselected += () => @@ -469,7 +469,7 @@ namespace Barotrauma.Items.Components RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) { - SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); + SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits".ToIdentifier(), RotationLimits, oldRotation)); } }; widget.MouseHeld += (deltaTime) => @@ -503,7 +503,7 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUI.Style.Green; + widget.color = GUIStyle.Green; prevAngle = maxRotation; }; widget.Deselected += () => @@ -513,7 +513,7 @@ namespace Barotrauma.Items.Components RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) { - SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); + SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits".ToIdentifier(), RotationLimits, oldRotation)); } }; widget.MouseHeld += (deltaTime) => @@ -654,8 +654,8 @@ namespace Barotrauma.Items.Components if (ShowChargeIndicator && PowerConsumption > 0.0f) { powerIndicator.Color = charged ? - (HasPowerToShoot() ? GUI.Style.Green : GUI.Style.Orange) : - GUI.Style.Red; + (HasPowerToShoot() ? GUIStyle.Green : GUIStyle.Orange) : + GUIStyle.Red; if (flashLowPower) { powerIndicator.BarSize = 1; @@ -693,7 +693,7 @@ namespace Barotrauma.Items.Components Rectangle rect = new Rectangle(invSlotPos.X, invSlotPos.Y, totalWidth, slotSize.Y); float inflate = MathHelper.Lerp(3, 8, (float)Math.Abs(Math.Sin(flashTimer * 5))); rect.Inflate(inflate, inflate); - Color color = GUI.Style.Red * Math.Max(0.5f, (float)Math.Sin(flashTimer * 12)); + Color color = GUIStyle.Red * Math.Max(0.5f, (float)Math.Sin(flashTimer * 12)); if (flashNoAmmo) { GUI.DrawRectangle(spriteBatch, rect, color, thickness: 3); @@ -701,7 +701,7 @@ namespace Barotrauma.Items.Components else if (flashLoaderBroken) { GUI.DrawRectangle(spriteBatch, rect, color, thickness: 3); - GUI.BrokenIcon.Draw(spriteBatch, rect.Center.ToVector2(), color, scale: rect.Height / GUI.BrokenIcon.size.Y); + GUIStyle.BrokenIcon.Value.Sprite.Draw(spriteBatch, rect.Center.ToVector2(), color, scale: rect.Height / GUIStyle.BrokenIcon.Value.Sprite.size.Y); GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("turretloaderbroken"), new Rectangle(invSlotPos.X + totalWidth + GUI.IntScale(10), invSlotPos.Y + slotSize.Y / 2 - GUI.IntScale(9), 0, 0)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 26665699e..4c5848a45 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -1,25 +1,25 @@ using System; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma.Items.Components { - partial class Wearable + partial class Wearable : Pickable, IServerSerializable { - private void GetDamageModifierText(ref string description, DamageModifier damageModifier, string afflictionIdentifier) + private void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) { int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); if (roundedValue == 0) { return; } - string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + string colorStr = XMLExtensions.ColorToString(GUIStyle.Green); - string afflictionName = - AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase))?.Name ?? - TextManager.Get($"afflictiontype.{afflictionIdentifier}", returnNull: true) ?? - afflictionIdentifier; + LocalizedString afflictionName = + AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier)?.Name ?? + TextManager.Get($"afflictiontype.{afflictionIdentifier}").Fallback(afflictionIdentifier.Value); description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; } - - public override void AddTooltipInfo(ref string name, ref string description) + + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { if (damageModifiers.Any(d => !MathUtils.NearlyEqual(d.DamageMultiplier, 1f) || !MathUtils.NearlyEqual(d.ProbabilityMultiplier, 1f)) || SkillModifiers.Any()) { @@ -35,11 +35,11 @@ namespace Barotrauma.Items.Components continue; } - foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) + foreach (Identifier afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) { GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); } - foreach (string afflictionType in damageModifier.ParsedAfflictionTypes) + foreach (Identifier afflictionType in damageModifier.ParsedAfflictionTypes) { GetDamageModifierText(ref description, damageModifier, afflictionType); } @@ -49,10 +49,10 @@ namespace Barotrauma.Items.Components { foreach (var skillModifier in SkillModifiers) { - string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + string colorStr = XMLExtensions.ColorToString(GUIStyle.Green); int roundedValue = (int)Math.Round(skillModifier.Value); if (roundedValue == 0) { continue; } - description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}‖color:end‖ {TextManager.Get("SkillName." + skillModifier.Key, true) ?? skillModifier.Key}"; + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}‖color:end‖ {TextManager.Get($"SkillName.{skillModifier.Key}").Fallback(skillModifier.Key.Value)}"; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 86116fbbd..19ea4b3b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -43,7 +43,7 @@ namespace Barotrauma } public float QuickUseTimer; - public string QuickUseButtonToolTip; + public LocalizedString QuickUseButtonToolTip; public bool IsMoving = false; private static Rectangle offScreenRect = new Rectangle(new Point(-1000, 0), Point.Zero); @@ -144,7 +144,7 @@ namespace Barotrauma { public static float UIScale { - get { return (GameMain.GraphicsWidth / 1920.0f + GameMain.GraphicsHeight / 1080.0f) / 2.5f * GameSettings.InventoryScale; } + get { return (GameMain.GraphicsWidth / 1920.0f + GameMain.GraphicsHeight / 1080.0f) / 2.5f * GameSettings.CurrentConfig.Graphics.InventoryScale; } } public static int ContainedIndicatorHeight @@ -215,8 +215,7 @@ namespace Barotrauma public Inventory Inventory; public readonly Item Item; public readonly bool IsSubSlot; - public string Tooltip { get; private set; } - public List TooltipRichTextData { get; private set;} + public RichString Tooltip { get; private set; } public int tooltipDisplayedCondition; @@ -250,23 +249,22 @@ namespace Barotrauma { itemsInSlot = ParentInventory.GetItemsAt(SlotIndex); } - TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item, itemsInSlot), out string newTooltip); - Tooltip = newTooltip; + Tooltip = GetTooltip(Item, itemsInSlot); tooltipDisplayedCondition = (int)Item.ConditionPercentage; } - private string GetTooltip(Item item, IEnumerable itemsInSlot) + private RichString GetTooltip(Item item, IEnumerable itemsInSlot) { if (item == null) { return null; } - string toolTip = ""; + LocalizedString toolTip = ""; if (GameMain.DebugDraw) { toolTip = item.ToString(); } else { - string description = item.Description; + LocalizedString description = item.Description; if (item.Prefab.Identifier == "idcard" || item.Tags.Contains("despawncontainer")) { string[] readTags = item.Tags.Split(','); @@ -288,7 +286,7 @@ namespace Barotrauma } else { - description = TextManager.GetWithVariables("IDCardNameJob", new string[2] { "[name]", "[job]" }, new string[2] { idName, idJob }, new bool[2] { false, true }); + description = TextManager.GetWithVariables("IDCardNameJob", ("[name]", idName, FormatCapitals.No), ("[job]", idJob, FormatCapitals.Yes)); } if (!string.IsNullOrEmpty(item.Description)) { @@ -297,7 +295,7 @@ namespace Barotrauma } } - string name = item.Name; + LocalizedString name = item.Name; foreach (ItemComponent component in item.Components) { component.AddTooltipInfo(ref name, ref description); @@ -314,13 +312,14 @@ namespace Barotrauma } } - string colorStr = XMLExtensions.ColorToString(item.SpawnedInCurrentOutpost && !item.AllowStealing ? GUI.Style.Red : Color.White); + string colorStr = (item.SpawnedInCurrentOutpost && !item.AllowStealing ? GUIStyle.Red : Color.White).ToStringHex(); toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; if (item.GetComponent() != null) { - // substring by to get rid of the empty space at start, text file should be adjusted - toolTip += $"\n{TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", "", fallBackTag: "itemname.quality3")?.Substring(1)}"; + toolTip += "\n" + TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", "") + .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", "")) + .TrimStart(); } if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) @@ -329,24 +328,24 @@ namespace Barotrauma } if (!item.IsFullCondition && !item.Prefab.HideConditionInTooltip) { - string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull)); + string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull)); toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; } - if (!string.IsNullOrEmpty(description)) { toolTip += '\n' + description; } - if (item.prefab.ContentPackage != GameMain.VanillaContent && item.prefab.ContentPackage != null) + if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } + if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null) { colorStr = XMLExtensions.ColorToString(Color.MediumPurple); - toolTip += $"\n‖color:{colorStr}‖{item.prefab.ContentPackage.Name}‖color:end‖"; + toolTip += $"\n‖color:{colorStr}‖{item.Prefab.ContentPackage.Name}‖color:end‖"; } } if (itemsInSlot.Count() > 1) { - string colorStr = XMLExtensions.ColorToString(GUI.Style.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameMain.Config.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; - colorStr = XMLExtensions.ColorToString(GUI.Style.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameMain.Config.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + string colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); + toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; + colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); + toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; } - return toolTip; + return RichString.Rich(toolTip); } } @@ -551,7 +550,7 @@ namespace Barotrauma { if (item != null) { - slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); if (!mouseDrag) { SoundPlayer.PlayUISound(GUISoundType.PickItem); @@ -1038,9 +1037,9 @@ namespace Barotrauma return CursorState.Default; } - protected static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle highlightedSlot, List richTextData = null) + protected static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle highlightedSlot) { - GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, richTextData); + GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot); } public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex) @@ -1155,7 +1154,7 @@ namespace Barotrauma { foreach (int i in indices) { - inventory.visualSlots[i]?.ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } break; @@ -1248,7 +1247,7 @@ namespace Barotrauma selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } } - selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } SoundPlayer.PlayUISound(GUISoundType.PickItem); } @@ -1289,7 +1288,7 @@ namespace Barotrauma } else { - if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } + if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } } @@ -1439,20 +1438,20 @@ namespace Barotrauma if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) { - var shadowSprite = GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0]; - string toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : + var shadowSprite = GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0]; + LocalizedString toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : Character.Controlled.FocusedItem != null ? - TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, true) : + TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes) : TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); - int textWidth = (int)Math.Max(GUI.Font.MeasureString(DraggingItems.First().Name).X, GUI.SmallFont.MeasureString(toolTip).X); + int textWidth = (int)Math.Max(GUIStyle.Font.MeasureString(DraggingItems.First().Name).X, GUIStyle.SmallFont.MeasureString(toolTip).X); int textSpacing = (int)(15 * GUI.Scale); Point shadowBorders = (new Point(40, 10)).Multiply(GUI.Scale); shadowSprite.Draw(spriteBatch, new Rectangle(itemPos.ToPoint() - new Point(iconSize / 2) - shadowBorders, new Point(iconSize + textWidth + textSpacing, iconSize) + shadowBorders.Multiply(2)), Color.Black * 0.8f); GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y - iconSize / 2), DraggingItems.First().Name, Color.White); GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y), toolTip, - color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUI.Style.Red : Color.LightGreen, - font: GUI.SmallFont); + color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUIStyle.Red : Color.LightGreen, + font: GUIStyle.SmallFont); } sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black, scale: scale); sprite.Draw(spriteBatch, @@ -1464,8 +1463,8 @@ namespace Barotrauma { Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f; string stackCountText = "x" + DraggingItems.Count; - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } } } @@ -1478,7 +1477,7 @@ namespace Barotrauma { selectedSlot.RefreshTooltip(); } - DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect, selectedSlot.TooltipRichTextData); + DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); } } @@ -1514,11 +1513,11 @@ namespace Barotrauma /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) { - slotColor = slot.IsHighlighted ? GUI.Style.EquipmentSlotColor : GUI.Style.EquipmentSlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUIStyle.EquipmentSlotColor : GUIStyle.EquipmentSlotColor * 0.8f; } else { - slotColor = slot.IsHighlighted ? GUI.Style.InventorySlotColor : GUI.Style.InventorySlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUIStyle.InventorySlotColor : GUIStyle.InventorySlotColor * 0.8f; }*/ if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } @@ -1526,7 +1525,7 @@ namespace Barotrauma if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown() && selectedSlot?.Slot == slot) { - GUI.DrawRectangle(spriteBatch, rect, GUI.Style.Red * 0.3f, isFilled: true); + GUI.DrawRectangle(spriteBatch, rect, GUIStyle.Red * 0.3f, isFilled: true); } bool canBePut = false; @@ -1550,7 +1549,7 @@ namespace Barotrauma } if (slot.MouseOn() && canBePut && selectedSlot?.Slot == slot) { - GUI.UIGlow.Draw(spriteBatch, rect, GUI.Style.Green); + GUIStyle.UIGlow.Draw(spriteBatch, rect, GUIStyle.Green); } if (item != null && drawItem) @@ -1571,7 +1570,7 @@ namespace Barotrauma conditionIndicatorArea.Inflate(-4, 0); } - var indicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator.Default"); + var indicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator.Default"); Sprite indicatorSprite = indicatorStyle?.GetDefaultSprite(); Sprite emptyIndicatorSprite = indicatorStyle?.GetSprite(GUIComponent.ComponentState.Hover); DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, conditionIndicatorArea, item.Condition / item.MaxCondition); @@ -1624,14 +1623,14 @@ namespace Barotrauma if (item.Quality != 0) { - var style = GUI.Style.GetComponentStyle("InnerGlowSmall"); + var style = GUIStyle.GetComponentStyle("InnerGlowSmall"); if (style == null) { - GUI.DrawRectangle(spriteBatch, rect, GUI.Style.GetQualityColor(item.Quality) * 0.7f); + GUI.DrawRectangle(spriteBatch, rect, GUIStyle.GetQualityColor(item.Quality) * 0.7f); } else { - style.Sprites[GUIComponent.ComponentState.None].FirstOrDefault()?.Draw(spriteBatch, rect, GUI.Style.GetQualityColor(item.Quality) * 0.5f); + style.Sprites[GUIComponent.ComponentState.None].FirstOrDefault()?.Draw(spriteBatch, rect, GUIStyle.GetQualityColor(item.Quality) * 0.5f); } } } @@ -1640,7 +1639,7 @@ namespace Barotrauma var slotIcon = parentItem?.GetComponent()?.GetSlotIcon(slotIndex); if (slotIcon != null) { - slotIcon.Draw(spriteBatch, rect.Center.ToVector2(), GUI.Style.EquipmentSlotIconColor, scale: Math.Min(rect.Width / slotIcon.size.X, rect.Height / slotIcon.size.Y) * 0.8f); + slotIcon.Draw(spriteBatch, rect.Center.ToVector2(), GUIStyle.EquipmentSlotIconColor, scale: Math.Min(rect.Width / slotIcon.size.X, rect.Height / slotIcon.size.Y) * 0.8f); } } } @@ -1653,7 +1652,7 @@ namespace Barotrauma if (slot.HighlightColor != Color.Transparent) { - GUI.UIGlow.Draw(spriteBatch, rect, slot.HighlightColor); + GUIStyle.UIGlow.Draw(spriteBatch, rect, slot.HighlightColor); } if (item != null && drawItem) @@ -1693,7 +1692,7 @@ namespace Barotrauma stealIcon.Draw( spriteBatch, new Vector2(rect.X + iconSize.X * 0.2f, rect.Bottom - iconSize.Y * 1.2f), - color: GUI.Style.Red, + color: GUIStyle.Red, scale: iconSize.X / stealIcon.size.X); } int maxStackSize = item.Prefab.MaxStackSize; @@ -1708,9 +1707,9 @@ namespace Barotrauma { Vector2 stackCountPos = new Vector2(rect.Right, rect.Bottom); string stackCountText = "x" + itemCount; - stackCountPos -= GUI.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + stackCountPos -= GUIStyle.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } } } @@ -1721,7 +1720,7 @@ namespace Barotrauma slot.InventoryKeyIndex != -1) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameMain.Config.InventoryKeyBind(slot.InventoryKeyIndex).Name, Color.Black, font: GUI.HotkeyFont); + GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, Color.Black, font: GUIStyle.HotkeyFont); } } @@ -1730,7 +1729,7 @@ namespace Barotrauma Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState, bool pulsate = false) { - Color backgroundColor = GUI.Style.ColorInventoryBackground; + Color backgroundColor = GUIStyle.ColorInventoryBackground; if (indicatorSprite == null) { @@ -1738,7 +1737,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, containedIndicatorArea, backgroundColor, true); GUI.DrawRectangle(spriteBatch, new Rectangle(containedIndicatorArea.X, containedIndicatorArea.Y, (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Height), - ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull) * 0.8f, true); + ToolBox.GradientLerp(containedState, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull) * 0.8f, true); GUI.DrawLine(spriteBatch, new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Y), new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Bottom), @@ -1763,7 +1762,7 @@ namespace Barotrauma if (containedState > 0.0f) { - Color indicatorColor = ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull); + Color indicatorColor = ToolBox.GradientLerp(containedState, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull); if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), @@ -1784,7 +1783,7 @@ namespace Barotrauma } else if (emptyIndicatorSprite != null) { - Color indicatorColor = GUI.Style.ColorInventoryEmptyOverlay; + Color indicatorColor = GUIStyle.ColorInventoryEmptyOverlay; if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } emptyIndicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 86b76aebb..011ec97e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -11,6 +11,7 @@ using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; +using System.Collections.Immutable; namespace Barotrauma { @@ -33,7 +34,7 @@ namespace Barotrauma } else { - IconStyle = GUI.Style.GetComponentStyle($"CampaignInteractionIcon.{interactionType}"); + IconStyle = GUIStyle.GetComponentStyle($"CampaignInteractionIcon.{interactionType}"); } } @@ -48,6 +49,8 @@ namespace Barotrauma private readonly Dictionary spriteAnimState = new Dictionary(); + public float DrawDepthOffset; + private bool fakeBroken; public bool FakeBroken { @@ -90,7 +93,7 @@ namespace Barotrauma if (itemInUseWarning == null) { itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "", - textColor: GUI.Style.Orange, color: Color.Black, + textColor: GUIStyle.Orange, color: Color.Black, textAlignment: Alignment.Center, style: "OuterGlow"); } return itemInUseWarning; @@ -101,7 +104,7 @@ namespace Barotrauma { get { - if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(Prefab.Subcategory)) { return false; } @@ -114,7 +117,7 @@ namespace Barotrauma public float GetDrawDepth() { - return GetDrawDepth(SpriteDepth, Sprite); + return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite); } public Color GetSpriteColor() @@ -147,7 +150,7 @@ namespace Barotrauma partial void SetActiveSpriteProjSpecific() { - activeSprite = prefab.sprite; + activeSprite = Prefab.Sprite; activeContainedSprite = null; Holdable holdable = GetComponent(); if (holdable != null && holdable.Attached) @@ -179,7 +182,7 @@ namespace Barotrauma } float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; - for (int i = 0; i < Prefab.BrokenSprites.Count;i++) + for (int i = 0; i < Prefab.BrokenSprites.Length;i++) { if (Prefab.BrokenSprites[i].FadeIn) { continue; } float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f; @@ -193,14 +196,14 @@ namespace Barotrauma partial void InitProjSpecific() { - Prefab.sprite?.EnsureLazyLoaded(); + Prefab.Sprite?.EnsureLazyLoaded(); Prefab.InventoryIcon?.EnsureLazyLoaded(); foreach (BrokenItemSprite brokenSprite in Prefab.BrokenSprites) { brokenSprite.Sprite.EnsureLazyLoaded(); } - foreach (var decorativeSprite in ((ItemPrefab)prefab).DecorativeSprites) + foreach (var decorativeSprite in Prefab.DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); @@ -269,7 +272,7 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); + Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUIStyle.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); @@ -283,7 +286,7 @@ namespace Barotrauma Vector2 drawOffset = Vector2.Zero; if (displayCondition < MaxCondition) { - for (int i = 0; i < Prefab.BrokenSprites.Count; i++) + for (int i = 0; i < Prefab.BrokenSprites.Length; i++) { if (Prefab.BrokenSprites[i].FadeIn) { @@ -320,7 +323,7 @@ namespace Barotrauma if (body == null) { - if (prefab.ResizeHorizontal || prefab.ResizeVertical) + if (Prefab.ResizeHorizontal || Prefab.ResizeVertical) { Vector2 size = new Vector2(rect.Width, rect.Height); if (color.A > 0) @@ -380,9 +383,11 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; - if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + bool flipX = flippedX && Prefab.CanSpriteFlipX; + bool flipY = flippedY && Prefab.CanSpriteFlipY; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? rotationRad : -rotationRad) * Scale; + if (flipX) { offset.X = -offset.X; } + if (flipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, rotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); @@ -518,7 +523,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, rectWorldPos, new Vector2(transformedTrigger.Width, transformedTrigger.Height), - GUI.Style.Green, + GUIStyle.Green, false, 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); @@ -529,8 +534,8 @@ namespace Barotrauma foreach (MapEntity e in linkedTo) { - bool isLinkAllowed = prefab.IsLinkAllowed(e.prefab); - Color lineColor = GUI.Style.Red * 0.5f; + bool isLinkAllowed = Prefab.IsLinkAllowed(e.Prefab); + Color lineColor = GUIStyle.Red * 0.5f; if (isLinkAllowed) { lineColor = e is Item i && (DisplaySideBySideWhenLinked || i.DisplaySideBySideWhenLinked) ? Color.Purple * 0.5f : Color.LightGreen * 0.5f; @@ -685,7 +690,7 @@ namespace Barotrauma Spacing = (int)(25 * GUI.Scale) }; - var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont) { UserData = this }; + var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; activeEditors.Add(itemEditor); itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) @@ -694,21 +699,17 @@ namespace Barotrauma var itemContainer = GetComponent(); if (itemContainer != null) { - var tagsField = itemEditor.Fields["Tags"].First().Parent; + var tagsField = itemEditor.Fields["Tags".ToIdentifier()].First().Parent; //find all the items that can be put inside the container and add their PreferredContainer identifiers/tags to the available tags - HashSet availableTags = new HashSet(); - foreach (MapEntityPrefab me in MapEntityPrefab.List) - { - if (!(me is ItemPrefab ip)) { continue; } - if (!itemContainer.CanBeContained(ip)) { continue; } - foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Primary)) { availableTags.Add(tag); } - foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Secondary)) { availableTags.Add(tag); } - } - //remove identifiers from the available container tags - //(otherwise the list will include many irrelevant options, - //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) - availableTags.RemoveWhere(t => MapEntityPrefab.List.Any(me => me.Identifier == t)); + ImmutableHashSet availableTags = ItemPrefab.Prefabs + .Where(ip => itemContainer.CanBeContained(ip)) + .SelectMany(ip => ip.PreferredContainers.SelectMany(pc => pc.Primary.Union(pc.Secondary))) + //remove identifiers from the available container tags + //(otherwise the list will include many irrelevant options, + //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) + .Where(t => !ItemPrefab.Prefabs.Any(ip => ip.Identifier == t)) + .ToImmutableHashSet(); new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") { OnClicked = (bt, userData) => { CreateTagPicker(tagsField.GetChild(), availableTags); return true; } @@ -717,14 +718,14 @@ namespace Barotrauma if (Linkable) { - var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUI.SmallFont); - var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUI.SmallFont); - string allowedItems = AllowedLinks.None() ? TextManager.Get("None") :string.Join(", ", AllowedLinks); + var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont); + var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUIStyle.SmallFont); + LocalizedString allowedItems = AllowedLinks.None() ? TextManager.Get("None") : string.Join(", ", AllowedLinks); itemsText.Text = TextManager.AddPunctuation(':', itemsText.Text, allowedItems); itemEditor.AddCustomContent(linkText, 1); itemEditor.AddCustomContent(itemsText, 2); - linkText.TextColor = GUI.Style.Orange; - itemsText.TextColor = GUI.Style.Orange; + linkText.TextColor = GUIStyle.Orange; + itemsText.TextColor = GUIStyle.Orange; } var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) @@ -795,7 +796,7 @@ namespace Barotrauma { GUITickBox tickBox = new GUITickBox(new RectTransform(new Point(listBox.Content.Rect.Width, 10)), TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.name")) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Selected = RemoveIfLinkedOutpostDoorInUse, ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"), OnSelected = (tickBox) => @@ -826,7 +827,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); - var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUI.SubHeadingFont) { UserData = ic }; + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUIStyle.SubHeadingFont) { UserData = ic }; componentEditor.Children.First().Color = Color.Black * 0.7f; activeEditors.Add(componentEditor); @@ -851,7 +852,7 @@ namespace Barotrauma { //TODO: add to localization var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), - relatedItem.Type.ToString() + " required", font: GUI.SmallFont) + relatedItem.Type.ToString() + " required", font: GUIStyle.SmallFont) { Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f) }; @@ -860,7 +861,7 @@ namespace Barotrauma GUITextBox namesBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight)) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Text = relatedItem.JoinedIdentifiers, OverflowClip = true }; @@ -890,7 +891,7 @@ namespace Barotrauma return editingHUD; } - private List GetUpgradeSprites(Upgrade upgrade) + private ImmutableArray GetUpgradeSprites(Upgrade upgrade) { var upgradeSprites = upgrade.Prefab.DecorativeSprites; @@ -908,7 +909,7 @@ namespace Barotrauma bool result = base.AddUpgrade(upgrade, createNetworkEvent); if (result && !upgrade.Disposed) { - List upgradeSprites = GetUpgradeSprites(upgrade); + var upgradeSprites = GetUpgradeSprites(upgrade); if (upgradeSprites.Any()) { @@ -923,9 +924,9 @@ namespace Barotrauma return result; } - private void CreateTagPicker(GUITextBox textBox, IEnumerable availableTags) + private void CreateTagPicker(GUITextBox textBox, IEnumerable availableTags) { - var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -940,10 +941,10 @@ namespace Barotrauma } }; - foreach (string availableTag in availableTags.ToList().OrderBy(t => t)) + foreach (var availableTag in availableTags.ToList().OrderBy(t => t)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, - ToolBox.LimitString(availableTag, GUI.Font, textList.Content.Rect.Width)) + ToolBox.LimitString(availableTag.Value, GUIStyle.Font, textList.Content.Rect.Width)) { UserData = availableTag }; @@ -998,11 +999,11 @@ namespace Barotrauma GameMain.GraphicsWidth, HUDLayoutSettings.InventoryTopY > 0 ? HUDLayoutSettings.InventoryTopY - 40 : GameMain.GraphicsHeight - 80)); + //System.Diagnostics.Debug.WriteLine("after: " + elementsToMove[0].Rect.ToString() + " " + elementsToMove[1].Rect.ToString()); foreach (ItemComponent ic in activeHUDs) { if (ic.GuiFrame == null) { continue; } - var linkUIToComponent = ic.GetLinkUIToComponent(); if (linkUIToComponent == null) { continue; } @@ -1041,7 +1042,7 @@ namespace Barotrauma foreach (MapEntity entity in linkedTo) { - if (prefab.IsLinkAllowed(entity.prefab) && entity is Item i) + if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i) { if (!i.DisplaySideBySideWhenLinked) { continue; } activeComponents.AddRange(i.components); @@ -1179,17 +1180,17 @@ namespace Barotrauma nameText += $" ({idName})"; } } - texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); + texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, false, false)); if (CampaignMode.BlocksInteraction(CampaignInteractionType)) { - texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameMain.Config.KeyBindText(InputType.Use)), Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, false, false)); } else { foreach (ItemComponent ic in components) { - if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } + if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } @@ -1205,12 +1206,12 @@ namespace Barotrauma color = Color.Cyan; } } - texts.Add(new ColoredText(ic.DisplayMsg, color, false, false)); + texts.Add(new ColoredText(ic.DisplayMsg.Value, color, false, false)); } } if (PlayerInput.IsShiftDown() && CrewManager.DoesItemHaveContextualOrders(this)) { - texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, false, false)); } return texts; } @@ -1350,7 +1351,7 @@ namespace Barotrauma ReadPropertyChange(msg, false); break; case NetEntityEvent.Type.Upgrade: - string identifier = msg.ReadString(); + Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); if (UpgradePrefab.Find(identifier) is { } upgradePrefab) { @@ -1458,7 +1459,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError(errorMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif GameAnalyticsManager.AddErrorEventOnce("Item.ClientReadPosition:nophysicsbody", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; @@ -1490,10 +1491,10 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = false (" + body.PhysEnabled + ")", e); - if (body.UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + body.UserData.GetType().ToString(), GUI.Style.Red); - if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", GUI.Style.Red); - else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", GUI.Style.Red); - if (body.FarseerBody.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", GUI.Style.Red); + if (body.UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + body.UserData.GetType().ToString(), GUIStyle.Red); + if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", GUIStyle.Red); + else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", GUIStyle.Red); + if (body.FarseerBody.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", GUIStyle.Red); } } @@ -1579,8 +1580,8 @@ namespace Barotrauma string tags = ""; if (tagsChanged) { - string[] addedTags = msg.ReadString().Split(','); - string[] removedTags = msg.ReadString().Split(','); + HashSet addedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet(); + HashSet removedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet(); if (itemPrefab != null) { tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags)); @@ -1600,7 +1601,7 @@ namespace Barotrauma if (itemPrefab == null) { string errorMsg = "Failed to spawn item, prefab not found (name: " + (itemName ?? "null") + ", identifier: " + (itemIdentifier ?? "null") + ")"; - errorMsg += "\n" + string.Join(", ", GameMain.Config.AllEnabledPackages.Select(cp => cp.Name)); + errorMsg += "\n" + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name)); GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:PrefabNotFound" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsManager.ErrorSeverity.Critical, errorMsg); @@ -1621,7 +1622,7 @@ namespace Barotrauma if (itemContainerIndex < 0 || itemContainerIndex >= parentItem.components.Count) { string errorMsg = - $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; + $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.Prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 274eabdf9..860e59d1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -1,8 +1,11 @@ -using FarseerPhysics; +using Barotrauma.IO; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -35,58 +38,175 @@ namespace Barotrauma public readonly Sprite Sprite; public readonly bool UseWhenAttached; public readonly DecorativeSpriteBehaviorType DecorativeSpriteBehavior; - public readonly string[] AllowedContainerIdentifiers; - public readonly string[] AllowedContainerTags; + public readonly ImmutableHashSet AllowedContainerIdentifiers; + public readonly ImmutableHashSet AllowedContainerTags; - public ContainedItemSprite(XElement element, string path = "", bool lazyLoad = false) + public ContainedItemSprite(ContentXElement element, string path = "", bool lazyLoad = false) { Sprite = new Sprite(element, path, lazyLoad: lazyLoad); UseWhenAttached = element.GetAttributeBool("usewhenattached", false); Enum.TryParse(element.GetAttributeString("decorativespritebehavior", "None"), ignoreCase: true, out DecorativeSpriteBehavior); - AllowedContainerIdentifiers = element.GetAttributeStringArray("allowedcontaineridentifiers", new string[0], convertToLowerInvariant: true); - AllowedContainerTags = element.GetAttributeStringArray("allowedcontainertags", new string[0], convertToLowerInvariant: true); + AllowedContainerIdentifiers = element.GetAttributeIdentifierArray("allowedcontaineridentifiers", Array.Empty()).ToImmutableHashSet(); + AllowedContainerTags = element.GetAttributeIdentifierArray("allowedcontainertags", Array.Empty()).ToImmutableHashSet(); } public bool MatchesContainer(Item container) { if (container == null) { return false; } - return AllowedContainerIdentifiers.Contains(container.prefab.Identifier) || - AllowedContainerTags.Any(t => container.prefab.Tags.Contains(t)); + return AllowedContainerIdentifiers.Contains(container.Prefab.Identifier) || + AllowedContainerTags.Any(t => container.Prefab.Tags.Contains(t)); } } - partial class ItemPrefab : MapEntityPrefab + partial class ItemPrefab : MapEntityPrefab, IImplementsVariants { - public List BrokenSprites = new List(); - public List DecorativeSprites = new List(); - public List ContainedSprites = new List(); - public Dictionary> DecorativeSpriteGroups = new Dictionary>(); - public Sprite InventoryIcon; - public Sprite MinimapIcon; - public Sprite UpgradePreviewSprite; - public Sprite InfectedSprite; - public Sprite DamagedInfectedSprite; + public ImmutableDictionary> UpgradeOverrideSprites { get; private set; } + public ImmutableArray BrokenSprites { get; private set; } + public ImmutableArray DecorativeSprites { get; private set; } + public ImmutableArray ContainedSprites { get; private set; } + public ImmutableDictionary> DecorativeSpriteGroups { get; private set; } + public Sprite InventoryIcon { get; private set; } + public Sprite MinimapIcon { get; private set; } + public Sprite UpgradePreviewSprite { get; private set; } + public Sprite InfectedSprite { get; private set; } + public Sprite DamagedInfectedSprite { get; private set; } public float UpgradePreviewScale = 1.0f; //only used to display correct color in the sub editor, item instances have their own property that can be edited on a per-item basis - [Serialize("1.0,1.0,1.0,1.0", false)] - public Color InventoryIconColor - { - get; - protected set; - } + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)] + public Color InventoryIconColor { get; protected set; } - [Serialize(true, false)] + [Serialize("", IsPropertySaveable.No)] + public string ImpactSoundTag { get; private set; } + + [Serialize(true, IsPropertySaveable.No)] public bool ShowInStatusMonitor { get; private set; } + private void ParseSubElementsClient(ContentXElement element, ItemPrefab variantOf) + { + UpgradePreviewSprite = null; + UpgradePreviewScale = 1f; + InventoryIcon = null; + MinimapIcon = null; + InfectedSprite = null; + DamagedInfectedSprite = null; + var upgradeOverrideSprites = new Dictionary>(); + var brokenSprites = new List(); + var decorativeSprites = new List(); + var containedSprites = new List(); + var decorativeSpriteGroups = new Dictionary>(); - [Serialize("", false)] - public string ImpactSoundTag { get; private set; } + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "upgradeoverride": + { + + var sprites = new List(); + foreach (var decorSprite in subElement.Elements()) + { + if (decorSprite.NameAsIdentifier() == "decorativesprite") + { + sprites.Add(new DecorativeSprite(decorSprite)); + } + } + upgradeOverrideSprites.Add(subElement.GetAttributeIdentifier("identifier", Identifier.Empty), sprites); + break; + } + case "upgradepreviewsprite": + { + string iconFolder = GetTexturePath(subElement, variantOf); + UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f); + } + break; + case "inventoryicon": + { + string iconFolder = GetTexturePath(subElement, variantOf); + InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "minimapicon": + { + string iconFolder = GetTexturePath(subElement, variantOf); + MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "infectedsprite": + { + string iconFolder = GetTexturePath(subElement, variantOf); + + InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "damagedinfectedsprite": + { + string iconFolder = GetTexturePath(subElement, variantOf); + + DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "brokensprite": + string brokenSpriteFolder = GetTexturePath(subElement, variantOf); + + var brokenSprite = new BrokenItemSprite( + new Sprite(subElement, brokenSpriteFolder, lazyLoad: true), + subElement.GetAttributeFloat("maxcondition", 0.0f), + subElement.GetAttributeBool("fadein", false), + subElement.GetAttributePoint("offset", Point.Zero)); + + int spriteIndex = 0; + for (int i = 0; i < brokenSprites.Count && brokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++) + { + spriteIndex = i; + } + brokenSprites.Insert(spriteIndex, brokenSprite); + break; + case "decorativesprite": + string decorativeSpriteFolder = GetTexturePath(subElement, variantOf); + + int groupID = 0; + DecorativeSprite decorativeSprite = null; + if (subElement.Attribute("texture") == null) + { + groupID = subElement.GetAttributeInt("randomgroupid", 0); + } + else + { + decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); + decorativeSprites.Add(decorativeSprite); + groupID = decorativeSprite.RandomGroupID; + } + if (!decorativeSpriteGroups.ContainsKey(groupID)) + { + decorativeSpriteGroups.Add(groupID, new List()); + } + decorativeSpriteGroups[groupID].Add(decorativeSprite); + + break; + case "containedsprite": + string containedSpriteFolder = GetTexturePath(subElement, variantOf); + var containedSprite = new ContainedItemSprite(subElement, containedSpriteFolder, lazyLoad: true); + if (containedSprite.Sprite != null) + { + containedSprites.Add(containedSprite); + } + break; + } + } + + UpgradeOverrideSprites = upgradeOverrideSprites.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + BrokenSprites = brokenSprites.ToImmutableArray(); + DecorativeSprites = decorativeSprites.ToImmutableArray(); + ContainedSprites = containedSprites.ToImmutableArray(); + DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + } public override void UpdatePlacing(Camera cam) { @@ -94,7 +214,7 @@ namespace Barotrauma if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } @@ -104,7 +224,7 @@ namespace Barotrauma { if (PlayerInput.PrimaryMouseButtonClicked()) { - var item = new Item(new Rectangle((int)position.X, (int)position.Y, (int)(sprite.size.X * Scale), (int)(sprite.size.Y * Scale)), this, Submarine.MainSub) + var item = new Item(new Rectangle((int)position.X, (int)position.Y, (int)(Sprite.size.X * Scale), (int)(Sprite.size.Y * Scale)), this, Submarine.MainSub) { Submarine = Submarine.MainSub }; @@ -128,7 +248,7 @@ namespace Barotrauma } else { - Vector2 placeSize = size * Scale; + Vector2 placeSize = Size * Scale; if (placePosition == Vector2.Zero) { @@ -137,9 +257,9 @@ namespace Barotrauma else { if (ResizeHorizontal) - placeSize.X = Math.Max(position.X - placePosition.X, size.X); + placeSize.X = Math.Max(position.X - placePosition.X, Size.X); if (ResizeVertical) - placeSize.Y = Math.Max(placePosition.Y - position.Y, size.Y); + placeSize.Y = Math.Max(placePosition.Y - position.Y, Size.Y); if (PlayerInput.PrimaryMouseButtonReleased()) { @@ -174,24 +294,24 @@ namespace Barotrauma if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } if (!ResizeHorizontal && !ResizeVertical) { - sprite.Draw(spriteBatch, new Vector2(position.X, -position.Y) + sprite.size / 2.0f * Scale, SpriteColor, scale: Scale); + Sprite.Draw(spriteBatch, new Vector2(position.X, -position.Y) + Sprite.size / 2.0f * Scale, SpriteColor, scale: Scale); } else { - Vector2 placeSize = size * Scale; + Vector2 placeSize = Size * Scale; if (placePosition != Vector2.Zero) { if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, placeSize.X); } if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, placeSize.Y); } position = placePosition; } - sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + Sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); } } @@ -199,19 +319,19 @@ namespace Barotrauma { if (!ResizeHorizontal && !ResizeVertical) { - sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale); + Sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale); } else { Vector2 position = Submarine.MouseToWorldGrid(Screen.Selected.Cam, Submarine.MainSub); - Vector2 placeSize = size * Scale; + Vector2 placeSize = Size * Scale; if (placePosition != Vector2.Zero) { if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, placeSize.X); } if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, placeSize.Y); } position = placePosition; } - sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + Sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 04169a6fa..80d814e5f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -16,79 +16,35 @@ namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { - - // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global, UnusedAutoPropertyAccessor.Global, MemberCanBePrivate.Global - internal class DamageParticle - { - [Serialize(defaultValue: "", isSaveable: false)] - public string Identifier { get; set; } = ""; - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MinRotation { get; set; } - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MaxRotation { get; set; } - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MinVelocity { get; set; } - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MaxVelocity { get; set; } - - [Serialize(defaultValue: "255,255,255,255", isSaveable: false)] - public Color ColorMultiplier { get; set; } - - private float RandRotation() => Rand.Range(MinRotation, MaxRotation); - private float RandVelocity() => Rand.Range(MinVelocity, MaxVelocity); - - public void Emit(Vector2 pos) - { - Particle particle = GameMain.ParticleManager.CreateParticle(Identifier, pos, RandRotation(), RandVelocity()); - if (particle != null) - { - particle.ColorMultiplier = ColorMultiplier.ToVector4(); - } - } - - public DamageParticle(XElement element) - { - SerializableProperty.DeserializeProperties(this, element); - } - } - public Sprite? branchAtlas, decayAtlas; public readonly Dictionary BranchSprites = new Dictionary(); public readonly List FlowerSprites = new List(), DamagedFlowerSprites = new List(); public readonly List HiddenFlowerSprites = new List(); public readonly List LeafSprites = new List(), DamagedLeafSprites = new List(); - public readonly List DamageParticles = new List(); - public readonly List DeathParticles = new List(); + public readonly List DamageParticles = new List(); + public readonly List DeathParticles = new List(); public static bool AlwaysShowBallastFloraSprite = false; - partial void LoadPrefab(XElement element) + partial void LoadPrefab(ContentXElement element) { - string? branchAtlasPath = element.GetAttributeString("branchatlas", null); - string? decayAtlasPath = element.GetAttributeString("decayatlas", null); - - if (branchAtlasPath != null) + if (element.GetAttributeContentPath("branchatlas") is { } branchAtlasPath) { - branchAtlas = new Sprite(branchAtlasPath, Rectangle.Empty); - } - - if (decayAtlasPath != null) - { - decayAtlas = new Sprite(decayAtlasPath, Rectangle.Empty); + branchAtlas = new Sprite(branchAtlasPath.Value, Rectangle.Empty); } - foreach (XElement subElement in element.Elements()) + if (element.GetAttributeContentPath("decayatlas") is { } decayAtlasPath) + { + decayAtlas = new Sprite(decayAtlasPath.Value, Rectangle.Empty); + } + + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "branchsprite": - var tileType = subElement.GetAttributeString("type", null); - VineTileType type = Enum.Parse(tileType); + var type = subElement.GetAttributeEnum("type", VineTileType.Stem); BranchSprites.Add(type, new VineSprite(subElement)); break; case "flowersprite": @@ -107,10 +63,10 @@ namespace Barotrauma.MapCreatures.Behavior DamagedLeafSprites.Add(new Sprite(subElement)); break; case "damageparticle": - DamageParticles.Add(new DamageParticle(subElement)); + DamageParticles.Add(new ParticleEmitter(subElement)); break; case "deathparticle": - DeathParticles.Add(new DamageParticle(subElement)); + DeathParticles.Add(new ParticleEmitter(subElement)); break; case "targets": LoadTargets(subElement); @@ -131,29 +87,59 @@ namespace Barotrauma.MapCreatures.Behavior } } - private void CreateDamageParticle(BallastFloraBranch branch, float damage) - { - Vector2 pos = GetWorldPosition() + branch.Position; - int amount = (int)Math.Clamp(damage / 10f, 1, 10); - for (int i = 0; i < amount; i++) + partial void UpdateDamage(float deltaTime) + { + foreach (BallastFloraBranch branch in Branches) { - foreach (DamageParticle particle in DamageParticles) + if (branch.AccumulatedDamage > 0) { - particle.Emit(pos); + CreateDamageParticle(branch, branch.AccumulatedDamage); + + if (GameMain.DebugDraw) + { + var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position; + GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUIStyle.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1); + } + } + if (Character.Controlled != null && Character.Controlled.CurrentHull == branch.CurrentHull && + branch.IsRoot && + (branch.AccumulatedDamage > 0.0f || branch.AccumulatedDamage < -0.1f)) + { + Character.Controlled.UpdateHUDProgressBar(this, + GetWorldPosition() + branch.Position, + branch.Health / branch.MaxHealth, + emptyColor: GUIStyle.HealthBarColorLow, + fullColor: GUIStyle.HealthBarColorHigh, + textTag: Prefab.DisplayName.Value); + } + branch.AccumulatedDamage = 0f; + if (branch.DamageVisualizationTimer > 0.0f) + { + branch.DamageVisualizationTimer -= deltaTime; + float t1 = (float)Timing.TotalTime * 0.2f + branch.Position.X / 100.0f; + float t2 = (float)Timing.TotalTime * 0.5f + branch.Position.Y / 100.0f; + branch.ShakeAmount = new Vector2( + PerlinNoise.GetPerlin(t1, t2) - 0.5f, + PerlinNoise.GetPerlin(t2, t1) - 0.5f) * 10.0f * branch.DamageVisualizationTimer; } } } - private void CreateDeathParticle(BallastFloraBranch branch) + private void CreateDamageParticle(BallastFloraBranch branch, float deltaTime) { Vector2 pos = GetWorldPosition() + branch.Position; - int amount = (int)Math.Clamp(branch.MaxHealth / 10f, 1, 10); - for (int i = 0; i < amount; i++) + foreach (var particleEmitter in DamageParticles) { - foreach (DamageParticle particle in DeathParticles) - { - particle.Emit(pos); - } + particleEmitter.Emit(deltaTime, pos, branch.CurrentHull); + } + } + + private void CreateDeathParticle(BallastFloraBranch branch, float deltaTime) + { + Vector2 pos = GetWorldPosition() + branch.Position; + foreach (var particleEmitter in DeathParticles) + { + particleEmitter.Emit(deltaTime, pos, branch.CurrentHull); } } @@ -169,25 +155,25 @@ namespace Barotrauma.MapCreatures.Behavior { Vector2 pos = Parent.Submarine.DrawPosition + ConvertUnits.ToDisplayUnits(body.Position); pos.Y = -pos.Y; - GUI.DrawRectangle(spriteBatch, pos, 32f, 32f, 0f, Color.Cyan, 0.1f, thickness: 1); + GUI.DrawRectangle(spriteBatch, pos, 32f, 32f, 0f, body.UserData is BallastFloraBranch { IsRoot: true } ? Color.Magenta : Color.Cyan, 0.1f, thickness: 1); } foreach (var (key, steps) in IgnoredTargets) { string label = $"Ignored \"{key.Name}\" for {steps} steps"; - var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(label); + var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(label); Vector2 targetPos = key.WorldPosition; targetPos.Y = -targetPos.Y; - GUI.DrawString(spriteBatch, targetPos - new Vector2(sizeX / 2f, sizeY), label, GUI.Style.Red, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, targetPos - new Vector2(sizeX / 2f, sizeY), label, GUIStyle.Red, font: GUIStyle.SubHeadingFont); } } foreach (BallastFloraBranch branch in Branches) { - Vector2 pos = Parent.DrawPosition + Offset + branch.Position; + Vector2 pos = Parent.DrawPosition + Offset + branch.Position + branch.ShakeAmount; pos.Y = -pos.Y; - float depth = BranchDepth; + float depth = branch.IsRootGrowth ? 0.2f : BranchDepth; float layer1 = depth + 0.01f, layer2 = depth + 0.02f, @@ -195,7 +181,7 @@ namespace Barotrauma.MapCreatures.Behavior VineSprite branchSprite = BranchSprites[branch.Type]; - Color branchColor = Color.White; + Color branchColor = (branch.IsRoot || branch.IsRootGrowth) ? RootColor : Color.White; if (GameMain.DebugDraw) { @@ -207,7 +193,13 @@ namespace Barotrauma.MapCreatures.Behavior pos1.Y = -pos1.Y; Vector2 pos2 = basePos - to; pos2.Y = -pos2.Y; - GUI.DrawLine(spriteBatch, pos1, pos2, GUI.Style.Yellow * 0.8f, width: 4); + GUI.DrawLine(spriteBatch, pos1, pos2, GUIStyle.Yellow * 0.8f, width: 4); + } + if (branch.ParentBranch != null) + { + Vector2 pos2 = Parent.DrawPosition + Offset + branch.ParentBranch.Position; + pos2.Y = -pos2.Y; + GUI.DrawLine(spriteBatch, pos, pos2, GUIStyle.Green * 0.8f, width: 3); } #endif @@ -235,8 +227,8 @@ namespace Barotrauma.MapCreatures.Behavior } } - var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(label); - GUI.DrawString(spriteBatch, pos - new Vector2(sizeX / 2f, branch.Rect.Height + sizeY), label, Color.White, font: GUI.SubHeadingFont); + var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(label); + GUI.DrawString(spriteBatch, pos - new Vector2(sizeX / 2f, branch.Rect.Height + sizeY), label, Color.White, font: GUIStyle.SubHeadingFont); } bool isDamaged = branch.Health < branch.MaxHealth; @@ -340,7 +332,7 @@ namespace Barotrauma.MapCreatures.Behavior case NetworkHeader.BranchRemove: int removedBranchId = msg.ReadInt32(); - BallastFloraBranch removedBranch = Branches.FirstOrDefault(b => b.ID == removedBranchId); + BallastFloraBranch? removedBranch = Branches.FirstOrDefault(b => b.ID == removedBranchId); if (removedBranch != null) { RemoveBranch(removedBranch); @@ -352,14 +344,11 @@ namespace Barotrauma.MapCreatures.Behavior break; case NetworkHeader.BranchDamage: - int damageBranchId = msg.ReadInt32(); - float damage = msg.ReadSingle(); float health = msg.ReadSingle(); - BallastFloraBranch damagedBranch = Branches.FirstOrDefault(b => b.ID == damageBranchId); + BallastFloraBranch? damagedBranch = Branches.FirstOrDefault(b => b.ID == damageBranchId); if (damagedBranch != null) { - CreateDamageParticle(damagedBranch, damage); damagedBranch.Health = health; } else @@ -370,6 +359,9 @@ namespace Barotrauma.MapCreatures.Behavior case NetworkHeader.Kill: Kill(); break; + case NetworkHeader.Remove: + Remove(); + break; } PowerConsumptionTimer = msg.ReadSingle(); @@ -378,15 +370,18 @@ namespace Barotrauma.MapCreatures.Behavior private BallastFloraBranch ReadBranch(IReadMessage msg) { int id = msg.ReadInt32(); - byte type = (byte) msg.ReadRangedInteger(0b0000, 0b1111); - byte sides = (byte) msg.ReadRangedInteger(0b0000, 0b1111); + byte type = (byte)msg.ReadRangedInteger(0b0000, 0b1111); + byte sides = (byte)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); int leafConfig = msg.ReadRangedInteger(0, 0xFFF); int maxHealth = msg.ReadUInt16(); int posX = msg.ReadInt32(), posY = msg.ReadInt32(); + int parentBranchIndex = msg.ReadInt32(); Vector2 pos = new Vector2(posX * VineTile.Size, posY * VineTile.Size); - return new BallastFloraBranch(this, pos, (VineTileType)type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) + BallastFloraBranch? parentBranch = parentBranchIndex < 0 || parentBranchIndex >= Branches.Count ? null : Branches[parentBranchIndex]; + + return new BallastFloraBranch(this, parentBranch, pos, (VineTileType)type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) { ID = id, MaxHealth = maxHealth, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index afc015edf..57979fdd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -46,7 +46,7 @@ namespace Barotrauma if (!editing || !ShowGaps || !SubEditorScreen.IsLayerVisible(this)) { return; } - Color clr = (open == 0.0f) ? GUI.Style.Red : Color.Cyan; + Color clr = (open == 0.0f) ? GUIStyle.Red : Color.Cyan; if (IsHighlighted) { clr = Color.Gold; } GUI.DrawRectangle( @@ -118,7 +118,7 @@ namespace Barotrauma GUI.DrawRectangle(sb, new Vector2(WorldRect.X - 5, -WorldRect.Y - 5), new Vector2(rect.Width + 10, rect.Height + 10), - GUI.Style.Red, + GUIStyle.Red, false, depth, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 6a01d168d..b14c94720 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; namespace Barotrauma @@ -107,7 +108,7 @@ namespace Barotrauma { CanTakeKeyBoardFocus = false }; - new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont); + new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont); PositionEditingHUD(); @@ -285,7 +286,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(drawRect.X, -drawRect.Y), new Vector2(rect.Width, rect.Height), - (IsHighlighted ? Color.LightBlue * 0.8f : GUI.Style.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + (IsHighlighted ? Color.LightBlue * 0.8f : GUIStyle.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); } GUI.DrawRectangle(spriteBatch, @@ -295,34 +296,34 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), - GUI.Style.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); if (GameMain.DebugDraw) { - GUI.SmallFont.DrawString(spriteBatch, "Pressure: " + ((int)pressure - rect.Y).ToString() + + GUIStyle.SmallFont.DrawString(spriteBatch, "Pressure: " + ((int)pressure - rect.Y).ToString() + " - Oxygen: " + ((int)OxygenPercentage), new Vector2(drawRect.X + 5, -drawRect.Y + 5), Color.White); - GUI.SmallFont.DrawString(spriteBatch, waterVolume + " / " + Volume, new Vector2(drawRect.X + 5, -drawRect.Y + 20), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, waterVolume + " / " + Volume, new Vector2(drawRect.X + 5, -drawRect.Y + 20), Color.White); GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * Math.Min(waterVolume / Volume, 1.0f))), Color.Cyan, true); if (WaterVolume > Volume) { float maxExcessWater = Volume * MaxCompress; - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUIStyle.Red, true); } GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, 100), Color.Black); foreach (FireSource fs in FireSources) { Rectangle fireSourceRect = new Rectangle((int)fs.WorldPosition.X, -(int)fs.WorldPosition.Y, (int)fs.Size.X, (int)fs.Size.Y); - GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Red, false, 0, 5); - GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); + GUI.DrawRectangle(spriteBatch, fireSourceRect, GUIStyle.Red, false, 0, 5); + GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUIStyle.Orange, false, 0, 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); } foreach (FireSource fs in FakeFireSources) { Rectangle fireSourceRect = new Rectangle((int)fs.WorldPosition.X, -(int)fs.WorldPosition.Y, (int)fs.Size.X, (int)fs.Size.Y); - GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Red, false, 0, 5); - GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); + GUI.DrawRectangle(spriteBatch, fireSourceRect, GUIStyle.Red, false, 0, 5); + GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUIStyle.Orange, false, 0, 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); } @@ -358,7 +359,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(currentHullRect.X, -currentHullRect.Y), new Vector2(connectedHullRect.X, -connectedHullRect.Y), - GUI.Style.Green, width: 2); + GUIStyle.Green, width: 2); } } } @@ -376,7 +377,7 @@ namespace Barotrauma if (section.ColorStrength < 0.01f || section.Color.A < 1) { continue; } - if (GameMain.DecalManager.GrimeSprites.Count == 0) + if (DecalManager.GrimeSprites.None()) { GUI.DrawRectangle(spriteBatch, new Vector2(drawOffset.X + rect.X + section.Rect.X, -(drawOffset.Y + rect.Y + section.Rect.Y)), @@ -387,7 +388,7 @@ namespace Barotrauma { Vector2 sectionPos = new Vector2(drawPos.X + section.Rect.Location.X, -(drawPos.Y + section.Rect.Location.Y)); Vector2 randomOffset = new Vector2(section.Noise.X - 0.5f, section.Noise.Y - 0.5f) * 15.0f; - var sprite = GameMain.DecalManager.GrimeSprites[i % GameMain.DecalManager.GrimeSprites.Count]; + var sprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{i % DecalManager.GrimeSpriteCount}"].Sprite; sprite.Draw(spriteBatch, sectionPos + randomOffset, section.GetStrengthAdjustedColor(), scale: 1.25f); } } @@ -648,7 +649,7 @@ namespace Barotrauma BallastFloraBehavior.NetworkHeader header = (BallastFloraBehavior.NetworkHeader) message.ReadByte(); if (header == BallastFloraBehavior.NetworkHeader.Spawn) { - string identifier = message.ReadString(); + Identifier identifier = message.ReadIdentifier(); float x = message.ReadSingle(); float y = message.ReadSingle(); BallastFlora = new BallastFloraBehavior(this, BallastFloraPrefab.Find(identifier), new Vector2(x, y), firstGrowth: true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index f20bd6797..0db354aea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -8,7 +8,7 @@ using System.Xml.Linq; namespace Barotrauma { - partial class ItemAssemblyPrefab + partial class ItemAssemblyPrefab : MapEntityPrefab { public void DrawIcon(SpriteBatch spriteBatch, GUICustomComponent guiComponent) { @@ -16,28 +16,28 @@ namespace Barotrauma float scale = Math.Min(drawArea.Width / (float)Bounds.Width, drawArea.Height / (float)Bounds.Height) * 0.9f; - foreach (Pair entity in DisplayEntities) + foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) { - if (entity.First is CoreEntityPrefab) { continue; } - Rectangle drawRect = entity.Second; - drawRect = new Rectangle( - (int)(drawRect.X * scale) + drawArea.Center.X, (int)((drawRect.Y) * scale) - drawArea.Center.Y, - (int)(drawRect.Width * scale), (int)(drawRect.Height * scale)); - entity.First.DrawPlacing(spriteBatch, drawRect, entity.First.Scale * scale); + var entityPrefab = MapEntityPrefab.FindByIdentifier(identifier); + if (entityPrefab is CoreEntityPrefab) { continue; } + var drawRect = new Rectangle( + (int)(rect.X * scale) + drawArea.Center.X, (int)((rect.Y) * scale) - drawArea.Center.Y, + (int)(rect.Width * scale), (int)(rect.Height * scale)); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale * scale); } } - public override void DrawPlacing(SpriteBatch spriteBatch, Camera cam) { base.DrawPlacing(spriteBatch, cam); - foreach (Pair entity in DisplayEntities) + foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) { - Rectangle drawRect = entity.Second; + var entityPrefab = MapEntityPrefab.Find(p => p.Identifier == identifier); + Rectangle drawRect = rect; drawRect.Location += placePosition != Vector2.Zero ? placePosition.ToPoint() : Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); - entity.First.DrawPlacing(spriteBatch, drawRect, entity.First.Scale); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs index ad29039b0..b7aae9022 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs @@ -90,7 +90,7 @@ namespace Barotrauma checkWallsTimer = Rand.Range(0.0f, CheckWallsInterval, Rand.RandSync.ClientOnly); - foreach (XElement subElement in prefab.Config.Elements()) + foreach (var subElement in prefab.Config.Elements()) { List deformationList = null; switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index b8ef5800c..7dc0b6ad4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -18,45 +18,46 @@ namespace Barotrauma private readonly List prefabs = new List(); private readonly List creatures = new List(); - public BackgroundCreatureManager(string configPath) - { - LoadConfig(new ContentFile(configPath, ContentType.BackgroundCreaturePrefabs)); - } - - public BackgroundCreatureManager(IEnumerable files) + public BackgroundCreatureManager(IEnumerable files) { foreach(var file in files) { - LoadConfig(file); + LoadConfig(file.Path); } } - private void LoadConfig(ContentFile config) + public BackgroundCreatureManager(string path) + { + DebugConsole.AddWarning($"Couldn't find any BackgroundCreaturePrefabs files, falling back to {path}"); + LoadConfig(ContentPath.FromRaw(null, path)); + } + + private void LoadConfig(ContentPath configPath) { try { - XDocument doc = XMLExtensions.TryLoadXml(config.Path); + XDocument doc = XMLExtensions.TryLoadXml(configPath); if (doc == null) { return; } - var mainElement = doc.Root; + var mainElement = doc.Root.FromPackage(configPath.ContentPackage); if (mainElement.IsOverride()) { - mainElement = doc.Root.FirstElement(); + mainElement = mainElement.FirstElement(); prefabs.Clear(); - DebugConsole.NewMessage($"Overriding all background creatures with '{config.Path}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.Yellow); } else if (prefabs.Any()) { - DebugConsole.NewMessage($"Loading additional background creatures from file '{config.Path}'"); + DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'"); } - foreach (XElement element in mainElement.Elements()) + foreach (var element in mainElement.Elements()) { prefabs.Add(new BackgroundCreaturePrefab(element)); }; } catch (Exception e) { - DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", config.Path), e); + DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs index 30939a39a..7c9ef97f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs @@ -12,52 +12,52 @@ namespace Barotrauma public readonly XElement Config; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Speed { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float WanderAmount { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float WanderZAmount { get; private set; } - [Serialize(1, true)] + [Serialize(1, IsPropertySaveable.Yes)] public int SwarmMin { get; private set; } - [Serialize(1, true)] + [Serialize(1, IsPropertySaveable.Yes)] public int SwarmMax { get; private set; } - [Serialize(200.0f, true)] + [Serialize(200.0f, IsPropertySaveable.Yes)] public float SwarmRadius { get; private set; } - [Serialize(0.2f, true)] + [Serialize(0.2f, IsPropertySaveable.Yes)] public float SwarmCohesion { get; private set; } - [Serialize(10.0f, true)] + [Serialize(10.0f, IsPropertySaveable.Yes)] public float MinDepth { get; private set; } - [Serialize(1000.0f, true)] + [Serialize(1000.0f, IsPropertySaveable.Yes)] public float MaxDepth { get; private set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DisableRotation { get; private set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DisableFlipping { get; private set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Scale { get; private set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Commonness { get; private set; } - [Serialize(1000, true)] + [Serialize(1000, IsPropertySaveable.Yes)] public int MaxCount { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float FlashInterval { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float FlashDuration { get; private set; } @@ -65,9 +65,9 @@ namespace Barotrauma /// Overrides the commonness of the object in a specific level type. /// Key = name of the level type, value = commonness in that level type. /// - public Dictionary OverrideCommonness = new Dictionary(); + public Dictionary OverrideCommonness = new Dictionary(); - public BackgroundCreaturePrefab(XElement element) + public BackgroundCreaturePrefab(ContentXElement element) { Name = element.Name.ToString(); @@ -75,7 +75,7 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -92,7 +92,7 @@ namespace Barotrauma DeformableLightSprite = new DeformableSprite(subElement, lazyLoad: true); break; case "overridecommonness": - string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); @@ -104,9 +104,10 @@ namespace Barotrauma public float GetCommonness(LevelGenerationParams generationParams) { - if (generationParams?.Identifier != null && + if (generationParams != null && + !generationParams.Identifier.IsEmpty && (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || - (generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + (!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) { return commonness; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index 7396a16b5..ce1348882 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -130,12 +130,12 @@ namespace Barotrauma SoundTriggers = new LevelTrigger[Prefab.Sounds.Count]; for (int i = 0; i < Prefab.Sounds.Count; i++) { - Sounds[i] = Submarine.LoadRoundSound(Prefab.Sounds[i].SoundElement, false); + Sounds[i] = RoundSound.Load(Prefab.Sounds[i].SoundElement, false); SoundTriggers[i] = Prefab.Sounds[i].TriggerIndex > -1 ? Triggers[Prefab.Sounds[i].TriggerIndex] : null; } int j = 0; - foreach (XElement subElement in Prefab.Config.Elements()) + foreach (var subElement in Prefab.Config.Elements()) { if (!subElement.Name.ToString().Equals("deformablesprite", StringComparison.OrdinalIgnoreCase)) { continue; } foreach (XElement animationElement in subElement.Elements()) @@ -151,7 +151,7 @@ namespace Barotrauma } VisibleOnSonar = Prefab.SonarDisruption > 0.0f || Prefab.OverrideProperties.Any(p => p != null && p.SonarDisruption > 0.0f) || - (Triggers != null && Triggers.Any(t => !MathUtils.NearlyEqual(t.Force, Vector2.Zero) && t.ForceMode != LevelTrigger.TriggerForceMode.LimitVelocity || !string.IsNullOrWhiteSpace(t.InfectIdentifier))); + (Triggers != null && Triggers.Any(t => !MathUtils.NearlyEqual(t.Force, Vector2.Zero) && t.ForceMode != LevelTrigger.TriggerForceMode.LimitVelocity || !t.InfectIdentifier.IsEmpty)); if (VisibleOnSonar && Triggers.Any()) { SonarRadius = Triggers.Select(t => t.ColliderRadius * 1.5f).Max(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index f4ecea93d..76bf3c231 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -206,7 +206,7 @@ namespace Barotrauma if (GameMain.DebugDraw) { - GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true); if (obj.Triggers == null) { continue; } foreach (LevelTrigger trigger in obj.Triggers) @@ -218,7 +218,7 @@ namespace Barotrauma if (flowForce.LengthSquared() > 1) { flowForce.Y = -flowForce.Y; - GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUI.Style.Orange, 0, 5); + GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5); } trigger.PhysicsBody.UpdateDrawPosition(); trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index ebf0a6c6c..19b490b62 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -9,15 +9,15 @@ using System.Xml.Linq; namespace Barotrauma { - partial class LevelObjectPrefab + partial class LevelObjectPrefab : PrefabWithUintIdentifier, ISerializableEntity { public class SoundConfig { - public readonly XElement SoundElement; + public readonly ContentXElement SoundElement; public readonly Vector2 Position; public readonly int TriggerIndex; - public SoundConfig(XElement element, int triggerIndex) + public SoundConfig(ContentXElement element, int triggerIndex) { SoundElement = element; Position = element.GetAttributeVector2("position", Vector2.Zero); @@ -67,14 +67,14 @@ namespace Barotrauma private set; } = new List(); - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { LoadElementsProjSpecific(element, -1); } - private void LoadElementsProjSpecific(XElement element, int parentTriggerIndex) + private void LoadElementsProjSpecific(ContentXElement element, int parentTriggerIndex) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -121,7 +121,7 @@ namespace Barotrauma SerializableProperty.SerializeProperties(this, element); - foreach (XElement subElement in element.Elements().ToList()) + foreach (var subElement in element.Elements().ToList()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -144,7 +144,7 @@ namespace Barotrauma { int elementIndex = 0; bool wasSaved = false; - foreach (XElement subElement in element.Elements().ToList()) + foreach (var subElement in element.Elements().ToList()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -175,13 +175,13 @@ namespace Barotrauma new XAttribute("maxcount", childObj.MaxCount))); } - foreach (KeyValuePair overrideCommonness in OverrideCommonness) + foreach (KeyValuePair overrideCommonness in OverrideCommonness) { bool elementFound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("overridecommonness", System.StringComparison.OrdinalIgnoreCase) - && subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, System.StringComparison.OrdinalIgnoreCase)) + && subElement.GetAttributeIdentifier("leveltype", Identifier.Empty) == overrideCommonness.Key) { subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); elementFound = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 8028f686a..475553ddf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -326,7 +326,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(nodeList[i - 1].X, -nodeList[i - 1].Y), new Vector2(nodeList[i].X, -nodeList[i].Y), - Color.Lerp(Color.Yellow, GUI.Style.Red, i / (float)nodeList.Count), 0, 10); + Color.Lerp(Color.Yellow, GUIStyle.Red, i / (float)nodeList.Count), 0, 10); } }*/ diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index eccf97ae5..0483137ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -388,7 +388,7 @@ namespace Barotrauma.Lights if (!BoundingBox.Contains(point)) { return false; } Vector2 center = (vertices[0].Pos + vertices[1].Pos + vertices[2].Pos + vertices[3].Pos) * 0.25f; - for (int i=0;i<4;i++) + for (int i = 0; i < 4; i++) { Vector2 segmentVector = vertices[(i + 1) % 4].Pos - vertices[i].Pos; Vector2 centerToVertex = center - vertices[i].Pos; @@ -695,7 +695,7 @@ namespace Barotrauma.Lights } ShadowVertexCount = 0; - for (int i=0;i<4;i++) + for (int i = 0; i < 4; i++) { if (!backFacing[i]) { continue; } int currentIndex = i; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index f7826794f..420a52d66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Lights { var pp = graphics.PresentationParameters; - currLightMapScale = GameMain.Config.LightMapScale; + currLightMapScale = GameSettings.CurrentConfig.Graphics.LightMapScale; LightMap?.Dispose(); LightMap = CreateRenderTarget(); @@ -120,15 +120,15 @@ namespace Barotrauma.Lights RenderTarget2D CreateRenderTarget() { return new RenderTarget2D(graphics, - (int)(GameMain.GraphicsWidth * GameMain.Config.LightMapScale), (int)(GameMain.GraphicsHeight * GameMain.Config.LightMapScale), false, + (int)(GameMain.GraphicsWidth * GameSettings.CurrentConfig.Graphics.LightMapScale), (int)(GameMain.GraphicsHeight * GameSettings.CurrentConfig.Graphics.LightMapScale), false, pp.BackBufferFormat, pp.DepthStencilFormat, pp.MultiSampleCount, RenderTargetUsage.DiscardContents); } LosTexture?.Dispose(); LosTexture = new RenderTarget2D(graphics, - (int)(GameMain.GraphicsWidth * GameMain.Config.LightMapScale), - (int)(GameMain.GraphicsHeight * GameMain.Config.LightMapScale), false, SurfaceFormat.Color, DepthFormat.None); + (int)(GameMain.GraphicsWidth * GameSettings.CurrentConfig.Graphics.LightMapScale), + (int)(GameMain.GraphicsHeight * GameSettings.CurrentConfig.Graphics.LightMapScale), false, SurfaceFormat.Color, DepthFormat.None); } public void AddLight(LightSource light) @@ -165,13 +165,13 @@ namespace Barotrauma.Lights { if (!LightingEnabled) { return; } - if (Math.Abs(currLightMapScale - GameMain.Config.LightMapScale) > 0.01f) + if (Math.Abs(currLightMapScale - GameSettings.CurrentConfig.Graphics.LightMapScale) > 0.01f) { //lightmap scale has changed -> recreate render targets CreateRenderTargets(graphics); } - Matrix spriteBatchTransform = cam.Transform * Matrix.CreateScale(new Vector3(GameMain.Config.LightMapScale, GameMain.Config.LightMapScale, 1.0f)); + Matrix spriteBatchTransform = cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f)); Matrix transform = cam.ShaderTransform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; @@ -187,6 +187,7 @@ namespace Barotrauma.Lights if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } if (light.ParentBody != null) { + light.ParentBody.UpdateDrawPosition(); light.Position = light.ParentBody.DrawPosition; if (light.ParentSub != null) { light.Position -= light.ParentSub.DrawPosition; } } @@ -501,7 +502,7 @@ namespace Barotrauma.Lights private Dictionary GetVisibleHulls(Camera cam) { visibleHulls.Clear(); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.HiddenInGame) { continue; } var drawRect = @@ -537,7 +538,7 @@ namespace Barotrauma.Lights Vector2 scale = new Vector2( MathHelper.Clamp(losOffset.Length() / 256.0f, 4.0f, 5.0f), 3.0f); - spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameMain.Config.LightMapScale, GameMain.Config.LightMapScale, 1.0f))); + spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f))); spriteBatch.Draw(visionCircle, new Vector2(ViewTarget.WorldPosition.X, -ViewTarget.WorldPosition.Y), null, Color.White, rotation, new Vector2(visionCircle.Width * 0.2f, visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 151b148dd..0eacc74f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -15,9 +15,9 @@ namespace Barotrauma.Lights public bool Persistent; - public Dictionary SerializableProperties { get; private set; } = new Dictionary(); + public Dictionary SerializableProperties { get; private set; } = new Dictionary(); - [Serialize("1.0,1.0,1.0,1.0", true, alwaysUseInstanceValues: true), Editable] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable] public Color Color { get; @@ -26,7 +26,7 @@ namespace Barotrauma.Lights private float range; - [Serialize(100.0f, true, alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] + [Serialize(100.0f, IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { get { return range; } @@ -43,19 +43,19 @@ namespace Barotrauma.Lights } } - [Serialize(1f, true), Editable(minValue: 0.01f, maxValue: 100f, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(minValue: 0.01f, maxValue: 100f, ValueStep = 0.1f, DecimalCount = 2)] public float Scale { get; set; } - [Serialize("0, 0", true), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] + [Serialize("0, 0", IsPropertySaveable.Yes), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] public Vector2 Offset { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] public float Rotation { get; set; } public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(Rotation)); private float flicker; - [Editable, Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] public float Flicker { get { return flicker; } @@ -65,7 +65,7 @@ namespace Barotrauma.Lights } } - [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] + [Editable, Serialize(1.0f, IsPropertySaveable.No, description: "How fast the light flickers.")] public float FlickerSpeed { get; @@ -73,7 +73,7 @@ namespace Barotrauma.Lights } private float pulseFrequency; - [Editable, Serialize(0.0f, true, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] public float PulseFrequency { get { return pulseFrequency; } @@ -84,7 +84,7 @@ namespace Barotrauma.Lights } private float pulseAmount; - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, true, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] public float PulseAmount { get { return pulseAmount; } @@ -95,7 +95,7 @@ namespace Barotrauma.Lights } private float blinkFrequency; - [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] public float BlinkFrequency { get { return blinkFrequency; } @@ -124,7 +124,7 @@ namespace Barotrauma.Lights private set; } - public XElement DeformableLightSpriteElement + public ContentXElement DeformableLightSpriteElement { get; private set; @@ -134,11 +134,11 @@ namespace Barotrauma.Lights //Can be used to make lamp sprites glow at full brightness even if the light itself is dim. public float? OverrideLightSpriteAlpha; - public LightSourceParams(XElement element) + public LightSourceParams(ContentXElement element) { Deserialize(element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -427,7 +427,7 @@ namespace Barotrauma.Lights private readonly PropertyConditional.Comparison comparison; private readonly List conditionals = new List(); - public LightSource (XElement element, ISerializableEntity conditionalTarget = null) + public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) : this(Vector2.Zero, 100.0f, Color.White, null) { lightSourceParams = new LightSourceParams(element); @@ -444,7 +444,7 @@ namespace Barotrauma.Lights } this.conditionalTarget = conditionalTarget; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1211,7 +1211,7 @@ namespace Barotrauma.Lights new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y + bounds), null) }; - for (int i=0;i<4;i++) + for (int i = 0; i < 4; i++) { GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3); } @@ -1283,16 +1283,16 @@ namespace Barotrauma.Lights if (CastShadows && Screen.Selected == GameMain.SubEditorScreen) { - GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 20, Vector2.One * 40, GUI.Style.Orange, isFilled: false); - GUI.DrawLine(spriteBatch, drawPos - Vector2.One * 20, drawPos + Vector2.One * 20, GUI.Style.Orange); - GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * 20, drawPos + new Vector2(1.0f, -1.0f) * 20, GUI.Style.Orange); + GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 20, Vector2.One * 40, GUIStyle.Orange, isFilled: false); + GUI.DrawLine(spriteBatch, drawPos - Vector2.One * 20, drawPos + Vector2.One * 20, GUIStyle.Orange); + GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * 20, drawPos + new Vector2(1.0f, -1.0f) * 20, GUIStyle.Orange); } //visualize light recalculations float timeSinceRecalculation = (float)Timing.TotalTime - lastRecalculationTime; if (timeSinceRecalculation < 0.1f) { - GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUI.Style.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true); + GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUIStyle.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true); GUI.DrawLine(spriteBatch, drawPos - Vector2.One * Range, drawPos + Vector2.One * Range, Color); GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * Range, drawPos + new Vector2(1.0f, -1.0f) * Range, Color); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 2dc444032..897e86a23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -1,8 +1,8 @@ -using Barotrauma.Items.Components; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; -using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -25,14 +25,14 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(WorldPosition.X, -WorldPosition.Y), new Vector2(e.WorldPosition.X, -e.WorldPosition.Y), - isLinkAllowed ? GUI.Style.Green * 0.5f : GUI.Style.Red * 0.5f, width: 3); + isLinkAllowed ? GUIStyle.Green * 0.5f : GUIStyle.Red * 0.5f, width: 3); } } public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, float alpha = 1.0f) { - Color color = (IsHighlighted) ? GUI.Style.Orange : GUI.Style.Green; - if (IsSelected) { color = GUI.Style.Red; } + Color color = (IsHighlighted) ? GUIStyle.Orange : GUIStyle.Green; + if (IsSelected) { color = GUIStyle.Red; } Vector2 pos = drawPos; @@ -98,16 +98,16 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("LinkedSub"), font: GUI.LargeFont); + TextManager.Get("LinkedSub"), font: GUIStyle.LargeFont); if (!inGame) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("LinkLinkedSub"), textColor: GUI.Style.Orange, font: GUI.SmallFont); + TextManager.Get("LinkLinkedSub"), textColor: GUIStyle.Orange, font: GUIStyle.SmallFont); } var pathContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true); - var pathBox = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), pathContainer.RectTransform), filePath, font: GUI.SmallFont); + var pathBox = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), pathContainer.RectTransform), filePath, font: GUIStyle.SmallFont); var reloadButton = new GUIButton(new RectTransform(new Vector2(0.25f / pathBox.RectTransform.RelativeSize.X, 1.0f), pathBox.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), TextManager.Get("ReloadLinkedSub"), style: "GUIButtonSmall") { @@ -132,20 +132,21 @@ namespace Barotrauma if (!File.Exists(pathBox.Text)) { new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariable("ReloadLinkedSubError", "[file]", pathBox.Text)); - pathBox.Flash(GUI.Style.Red); + pathBox.Flash(GUIStyle.Red); pathBox.Text = filePath; return false; } XDocument doc = SubmarineInfo.OpenFile(pathBox.Text); - if (doc == null || doc.Root == null) return false; + if (doc == null || doc.Root == null) { return false; } doc.Root.SetAttributeValue("filepath", pathBox.Text); - pathBox.Flash(GUI.Style.Green); + pathBox.Flash(GUIStyle.Green); GenerateWallVertices(doc.Root); saveElement = doc.Root; saveElement.Name = "LinkedSubmarine"; + CargoCapacity = doc.Root.GetAttributeInt("cargocapacity", 0); filePath = pathBox.Text; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 2c34707c6..0f85e7fda 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.Xna.Framework.Input; @@ -65,10 +66,7 @@ namespace Barotrauma private float connectionHighlightState; - private (Rectangle targetArea, string tip)? tooltip; - private string sanitizedTooltip; - private List tooltipRichTextData; - private string prevTooltip; + private (Rectangle targetArea, RichString tip)? tooltip; private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; @@ -136,7 +134,7 @@ namespace Barotrauma for (int y = 0; y < tilesY; y++) { var biome = GetBiome(x * tileSize.X); - List tileList = null; + ImmutableArray tileList; if (generationParams.MapTiles.ContainsKey(biome.Identifier)) { tileList = generationParams.MapTiles[biome.Identifier]; @@ -146,7 +144,7 @@ namespace Barotrauma tileList = generationParams.MapTiles.Values.First(); missingBiomes.Add(biome); } - mapTiles[x, y] = tileList[x % tileList.Count]; + mapTiles[x, y] = tileList[x % tileList.Length]; } } @@ -208,7 +206,7 @@ namespace Barotrauma { if (location == null) { return; } - var mapTile = generationParams.MapTiles.Values.FirstOrDefault()?.FirstOrDefault(); + var mapTile = generationParams.MapTiles.Values.FirstOrDefault().FirstOrDefault(); if (mapTile == null) { return; } Vector2 mapTileSize = mapTile.size * generationParams.MapTileScale; @@ -253,12 +251,12 @@ namespace Barotrauma location.LastTypeChangeMessage = msg; if (GameMain.Client != null) { - GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName")); + GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName").Value); } else { GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage( - TextManager.Get("RadioAnnouncerName"), + TextManager.Get("RadioAnnouncerName").Value, msg, Networking.ChatMessageType.Default, sender: null); @@ -617,7 +615,7 @@ namespace Barotrauma { Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width; - generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUI.Style.Red, scale: typeChangeIconScale * zoom); + generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUIStyle.Red, scale: typeChangeIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { @@ -646,8 +644,8 @@ namespace Barotrauma Vector2 dPos = pos; dPos.Y += 48; string name = $"Reputation: {location.Name}"; - Vector2 nameSize = GUI.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, dPos, name, Color.White, Color.Black * 0.8f, 4, font: GUI.SmallFont); + Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); + GUI.DrawString(spriteBatch, dPos, name, Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); dPos.Y += nameSize.Y + 16; Rectangle bgRect = new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32); @@ -656,8 +654,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, (int)(location.Reputation.NormalizedValue * 255), 32), barColor, isFilled: true); string reputationValue = ((int)location.Reputation.Value).ToString(); - Vector2 repValueSize = GUI.SubHeadingFont.MeasureString(reputationValue); - GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUI.SubHeadingFont); + Vector2 repValueSize = GUIStyle.SubHeadingFont.MeasureString(reputationValue); + GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); } } @@ -670,12 +668,7 @@ namespace Barotrauma if (tooltip != null) { - if (tooltip.Value.tip != prevTooltip) - { - prevTooltip = tooltip.Value.tip; - tooltipRichTextData = RichTextData.GetRichTextData(tooltip.Value.tip, out sanitizedTooltip); - } - GUIComponent.DrawToolTip(spriteBatch, sanitizedTooltip, tooltip.Value.targetArea, tooltipRichTextData); + GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea); drawRadiationTooltip = false; } else if (HighlightedLocation != null) @@ -685,35 +678,35 @@ namespace Barotrauma pos.X += 50 * zoom; pos.X = (int)pos.X; pos.Y = (int)pos.Y; - Vector2 nameSize = GUI.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = string.IsNullOrEmpty(HighlightedLocation.Type.Name) ? Vector2.Zero : GUI.Font.MeasureString(HighlightedLocation.Type.Name); + Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); + Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; - string repLabelText = null, repValueText = null; + LocalizedString repLabelText = null, repValueText = null; Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; if (showReputation) { repLabelText = TextManager.Get("reputation"); - repLabelSize = GUI.Font.MeasureString(repLabelText); + repLabelSize = GUIStyle.Font.MeasureString(repLabelText); repBarSize = new Vector2(GUI.IntScale(200), repLabelSize.Y); size.Y += 2 * repLabelSize.Y + GUI.IntScale(5) + repBarSize.Y; repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); - size.X = Math.Max(size.X, repBarSize.X + GUI.Font.MeasureString(repValueText).X + GUI.IntScale(10)); + size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); } - GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( + GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( spriteBatch, new Rectangle((int)(pos.X - 60 * GUI.Scale), (int)(pos.Y - size.Y), (int)(size.X + 120 * GUI.Scale), (int)(size.Y * 2.2f)), Color.Black * hudVisibility); var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUI.Style.TextColor * hudVisibility * 1.5f, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); topLeftPos += new Vector2(0.0f, nameSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUI.Style.TextColor * hudVisibility * 1.5f); + GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f); if (showReputation) { topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, repLabelText, GUI.Style.TextColor * hudVisibility * 1.5f); + GUI.DrawString(spriteBatch, topLeftPos, repLabelText.Value, GUIStyle.TextColorNormal * hudVisibility * 1.5f); topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); - GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); + GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); } } @@ -763,7 +756,7 @@ namespace Barotrauma generationParams.SmallLevelConnectionLength, generationParams.LargeLevelConnectionLength, connection.Length); - connectionColor = ToolBox.GradientLerp(sizeFactor, Color.LightGreen, GUI.Style.Orange, GUI.Style.Red); + connectionColor = ToolBox.GradientLerp(sizeFactor, Color.LightGreen, GUIStyle.Orange, GUIStyle.Red); } else if (overrideColor.HasValue) { @@ -894,14 +887,14 @@ namespace Barotrauma } if (GameMain.GameSession?.Campaign?.UpgradeManager != null) { - var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth"); + var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth".ToIdentifier()); if (hullUpgradePrefab != null) { int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); int currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); if (pendingLevel > currentLevel) { - string updateValueStr = hullUpgradePrefab.SourceElement?.Element("Structure")?.GetAttributeString("crushdepth", null); + string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null); if (!string.IsNullOrEmpty(updateValueStr)) { subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel - currentLevel, updateValueStr); @@ -934,8 +927,8 @@ namespace Barotrauma { var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; var unlockEvent = - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && string.IsNullOrEmpty(ep.BiomeIdentifier)); + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); if (unlockEvent != null) { @@ -943,15 +936,15 @@ namespace Barotrauma Faction unlockFaction = null; if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)); + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); unlockReputation = unlockFaction?.Reputation; } DrawIcon( "LockedLocationConnection", (int)(28 * zoom), TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", - new string[] { "[requiredreputation]", "[currentreputation]" }, - new string[] { Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true), unlockReputation.GetFormattedReputationText(addColorTags: true) })); + ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true)))); } else { @@ -968,9 +961,9 @@ namespace Barotrauma if (crushDepthWarningIconStyle != null) { DrawIcon(crushDepthWarningIconStyle, (int)(32 * zoom), - TextManager.Get(tooltip) - .Replace("[initialdepth]", $"‖color:gui.orange‖{(int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)}‖end‖") - .Replace("[submarinecrushdepth]", $"‖color:gui.orange‖{(int)subCrushDepth}‖end‖")); + RichString.Rich(TextManager.GetWithVariables(tooltip, + ("[initialdepth]", $"‖color:gui.orange‖{(int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)}‖end‖"), + ("[submarinecrushdepth]", $"‖color:gui.orange‖{(int)subCrushDepth}‖end‖")))); } } @@ -983,14 +976,14 @@ namespace Barotrauma } } - void DrawIcon(string iconStyle, int iconSize, string tooltipText) + void DrawIcon(string iconStyle, int iconSize, LocalizedString tooltipText) { Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize; iconPos += (iconDiff * -(iconCount - 1) / 2.0f) + iconDiff * iconIndex; - var style = GUI.Style.GetComponentStyle(iconStyle); + var style = GUIStyle.GetComponentStyle(iconStyle); bool mouseOn = Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize && IsPreferredTooltip(iconPos); Sprite iconSprite = style.GetDefaultSprite(); iconSprite.Draw(spriteBatch, iconPos, (mouseOn ? style.HoverColor : style.Color) * 0.7f, @@ -1018,10 +1011,10 @@ namespace Barotrauma GUI.DrawString(spriteBatch, new Vector2(rect.Right - GUI.IntScale(170), rect.Y + GUI.IntScale(5)), - "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUI.SmallFont); + "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, new Vector2(rect.X + GUI.IntScale(15), rect.Bottom - GUI.IntScale(25)), - "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUI.SmallFont); + "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); } private void UpdateMapAnim(MapAnim anim, float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs index 990d91775..2ed19962f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs @@ -7,9 +7,9 @@ namespace Barotrauma { internal partial class Radiation { - private static readonly string radiationTooltip = TextManager.Get("RadiationTooltip"); + private static readonly LocalizedString radiationTooltip = TextManager.Get("RadiationTooltip"); private static float spriteIndex; - private readonly SpriteSheet sheet = GUI.Style.RadiationAnimSpriteSheet; + private readonly SpriteSheet sheet = GUIStyle.RadiationAnimSpriteSheet; private int maxFrames => sheet.FrameCount + 1; private bool isHovingOver; @@ -18,7 +18,7 @@ namespace Barotrauma { if (!Enabled) { return; } - UISprite uiSprite = GUI.Style.RadiationSprite; + UISprite uiSprite = GUIStyle.Radiation; var (offsetX, offsetY) = Map.DrawOffset * zoom; var (centerX, centerY) = container.Center.ToVector2(); var (halfSizeX, halfSizeY) = new Vector2(container.Width / 2f, container.Height / 2f) * zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 1c9c15096..0b0bec78b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -610,7 +610,7 @@ namespace Barotrauma foreach (MapEntity entity in highlightedEntities) { - var tooltip = string.Empty; + LocalizedString tooltip = string.Empty; if (wiringMode && entity is Item item) { @@ -622,9 +622,9 @@ namespace Barotrauma var conn = wire.Connections[i]; if (conn != null) { - string[] tags = { "[item]", "[pin]" }; - string[] values = { conn.Item?.Name, conn.Name }; - tooltip += TextManager.GetWithVariables("wirelistformat",tags , values); + tooltip += TextManager.GetWithVariables("wirelistformat", + ("[item]", conn.Item?.Name), + ("[pin]", conn.Name)); } if (i != wire.Connections.Length - 1) { tooltip += '\n'; } } @@ -632,7 +632,7 @@ namespace Barotrauma } var textBlock = new GUITextBlock(new RectTransform(new Point(highlightedListBox.Content.Rect.Width, 15), highlightedListBox.Content.RectTransform), - ToolBox.LimitString(entity.Name, GUI.SmallFont, 140), font: GUI.SmallFont) + ToolBox.LimitString(entity.Name, GUIStyle.SmallFont, 140), font: GUIStyle.SmallFont) { ToolTip = tooltip, UserData = entity @@ -803,7 +803,7 @@ namespace Barotrauma break; } } - e.prefab?.DrawPlacing(spriteBatch, + e.Prefab?.DrawPlacing(spriteBatch, new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); GUI.DrawRectangle(spriteBatch, new Vector2(e.WorldRect.X, -e.WorldRect.Y) + moveAmount, @@ -830,7 +830,7 @@ namespace Barotrauma new Vector2(posX, posY + sizeY) }; - Color selectionColor = GUI.Style.Blue; + Color selectionColor = GUIStyle.Blue; float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom); GUI.DrawFilledRectangle(spriteBatch, corners[0], selectionSize, selectionColor * 0.1f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index e7adb8e8d..daf65126a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -5,15 +5,13 @@ using System.Collections.Generic; namespace Barotrauma { - abstract partial class MapEntityPrefab : IPrefab, IDisposable + abstract partial class MapEntityPrefab : PrefabWithUintIdentifier { - public readonly Dictionary> UpgradeOverrideSprites = new Dictionary>(); - public virtual void UpdatePlacing(Camera cam) { if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } @@ -47,7 +45,7 @@ namespace Barotrauma placePosition = Vector2.Zero; if (!PlayerInput.IsShiftDown()) { - selected = null; + Selected = null; } } @@ -97,7 +95,7 @@ namespace Barotrauma } public void DrawListLine(SpriteBatch spriteBatch, Vector2 pos, Color color) { - GUI.Font.DrawString(spriteBatch, originalName, pos, color); + GUIStyle.Font.DrawString(spriteBatch, OriginalName, pos, color); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs new file mode 100644 index 000000000..5f4645cac --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -0,0 +1,137 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Xml.Linq; +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + class RoundSound + { + public Sound? Sound; + public readonly float Volume; + public readonly float Range; + public readonly Vector2 FrequencyMultiplierRange; + public readonly bool Stream; + public readonly bool IgnoreMuffling; + + public readonly string? Filename; + + private RoundSound(ContentXElement element, Sound sound) + { + Filename = sound.Filename; + Sound = sound; + Stream = sound.Stream; + Range = element.GetAttributeFloat("range", 1000.0f); + Volume = element.GetAttributeFloat("volume", 1.0f); + FrequencyMultiplierRange = new Vector2(1.0f); + string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0"))!; + if (!freqMultAttr.Contains(',')) + { + if (float.TryParse(freqMultAttr, NumberStyles.Any, CultureInfo.InvariantCulture, out float freqMult)) + { + FrequencyMultiplierRange = new Vector2(freqMult); + } + } + else + { + var freqMult = XMLExtensions.ParseVector2(freqMultAttr, false); + if (freqMult.Y >= 0.25f) + { + FrequencyMultiplierRange = freqMult; + } + } + if (FrequencyMultiplierRange.Y > 4.0f) + { + DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); + } + IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); + } + + public float GetRandomFrequencyMultiplier() + { + return Rand.Range(FrequencyMultiplierRange.X, FrequencyMultiplierRange.Y); + } + + private static readonly List roundSounds = new List(); + public static RoundSound? Load(ContentXElement element, bool stream = false) + { + if (GameMain.SoundManager?.Disabled ?? true) { return null; } + + var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound"); + + if (filename is null) + { + string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return null; + } + + Sound? existingSound = roundSounds.Find(s => s.Filename == filename?.FullPath && s.Stream == stream && s.Sound is { Disposed: false })?.Sound; + + if (existingSound is null) + { + try + { + existingSound = GameMain.SoundManager.LoadSound(filename?.FullPath, stream); + if (existingSound == null) { return null; } + } + catch (System.IO.FileNotFoundException e) + { + string errorMsg = "Failed to load sound file \"" + filename + "\"."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return null; + } + } + + RoundSound newSound = new RoundSound(element, existingSound); + + roundSounds.Add(newSound); + return newSound; + } + + public static void Reload(RoundSound roundSound) + { + Sound? existingSound = roundSounds.Find(s => s.Filename == roundSound.Filename && s.Stream == roundSound.Stream && s.Sound is { Disposed: false })?.Sound; + if (existingSound == null) + { + try + { + existingSound = GameMain.SoundManager.LoadSound(roundSound.Filename, roundSound.Stream); + } + catch (System.IO.FileNotFoundException e) + { + string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } + } + roundSound.Sound = existingSound; + } + + private static void Remove(RoundSound roundSound) + { + #warning TODO: what is going on here???? + roundSound.Sound?.Dispose(); + + if (roundSounds.Contains(roundSound)) { roundSounds.Remove(roundSound); } + foreach (RoundSound otherSound in roundSounds) + { + if (otherSound.Sound == roundSound.Sound) { otherSound.Sound = null; } + } + } + + public static void RemoveAllRoundSounds() + { + for (int i = roundSounds.Count - 1; i >= 0; i--) + { + Remove(roundSounds[i]); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 55bba4cda..339d904b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -26,7 +26,7 @@ namespace Barotrauma { get { - if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(Prefab.Subcategory)) { return false; } @@ -38,9 +38,9 @@ namespace Barotrauma } #if DEBUG - [Editable, Serialize("", true)] + [Editable, Serialize("", IsPropertySaveable.Yes)] #else - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] #endif public string SpecialTag { @@ -50,7 +50,7 @@ namespace Barotrauma partial void InitProjSpecific() { - Prefab.sprite?.EnsureLazyLoaded(); + Prefab.Sprite?.EnsureLazyLoaded(); Prefab.BackgroundSprite?.EnsureLazyLoaded(); foreach (var decorativeSprite in Prefab.DecorativeSprites) @@ -120,13 +120,13 @@ namespace Barotrauma { CanTakeKeyBoardFocus = false }; - var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont) { UserData = this }; + var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; if (Submarine.MainSub?.Info?.Type == SubmarineType.OutpostModule) { GUITickBox tickBox = new GUITickBox(new RectTransform(new Point(listBox.Content.Rect.Width, 10)), TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.name")) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Selected = RemoveIfLinkedOutpostDoorInUse, ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"), OnSelected = (tickBox) => @@ -246,7 +246,7 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (prefab.sprite == null) { return; } + if (Prefab.Sprite == null) { return; } if (editing) { @@ -265,17 +265,17 @@ namespace Barotrauma private float GetRealDepth() { - return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; + return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : Prefab.Sprite.Depth; } public float GetDrawDepth() { - return GetDrawDepth(GetRealDepth(), prefab.sprite); + return GetDrawDepth(GetRealDepth(), Prefab.Sprite); } private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) { - if (prefab.sprite == null) { return; } + if (Prefab.Sprite == null) { return; } if (editing) { if (!SubEditorScreen.IsLayerVisible(this)) { return; } @@ -284,7 +284,7 @@ namespace Barotrauma } else if (HiddenInGame) { return; } - Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted ? GUI.Style.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; + Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted ? GUIStyle.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; if (IsSelected && editing) { @@ -371,8 +371,8 @@ namespace Barotrauma if (back == GetRealDepth() > 0.5f) { - SpriteEffects oldEffects = prefab.sprite.effects; - prefab.sprite.effects ^= SpriteEffects; + SpriteEffects oldEffects = Prefab.Sprite.effects; + Prefab.Sprite.effects ^= SpriteEffects; for (int i = 0; i < Sections.Length; i++) { @@ -410,10 +410,10 @@ namespace Barotrauma if (FlippedX && IsHorizontal) { sectionOffset.X = drawSection.Right - rect.Right; } if (FlippedY && !IsHorizontal) { sectionOffset.Y = (rect.Y - rect.Height) - (drawSection.Y - drawSection.Height); } - sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, prefab.sprite.SourceRect.Width); - sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.sprite.SourceRect.Height); + sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, Prefab.Sprite.SourceRect.Width); + sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, Prefab.Sprite.SourceRect.Height); - prefab.sprite.DrawTiled( + Prefab.Sprite.DrawTiled( spriteBatch, new Vector2(drawSection.X + drawOffset.X, -(drawSection.Y + drawOffset.Y)), new Vector2(drawSection.Width, drawSection.Height), @@ -429,10 +429,10 @@ namespace Barotrauma float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, prefab.sprite.effects, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, Prefab.Sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - Prefab.Sprite.Depth), 0.999f)); } - prefab.sprite.effects = oldEffects; + Prefab.Sprite.effects = oldEffects; } if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.5f) @@ -473,13 +473,13 @@ namespace Barotrauma DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) { - for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Count; i++) + for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Length; i++) { var decorativeSprite = Prefab.DecorativeSpriteGroups[spriteGroup][i]; if (decorativeSprite == null) { continue; } if (spriteGroup > 0) { - int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Count; + int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Length; if (i != activeSpriteIndex) { spriteAnimState[decorativeSprite].IsActive = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 20e6c3b99..ee708d2ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -2,25 +2,22 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class StructurePrefab : MapEntityPrefab { - public Color BackgroundSpriteColor - { - get; - private set; - } + public readonly Color BackgroundSpriteColor; - public List DecorativeSprites = new List(); - public Dictionary> DecorativeSpriteGroups = new Dictionary>(); + public readonly ImmutableArray DecorativeSprites; + public readonly ImmutableDictionary> DecorativeSpriteGroups; public override void UpdatePlacing(Camera cam) { if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } @@ -65,7 +62,7 @@ namespace Barotrauma placePosition = Vector2.Zero; if (!PlayerInput.IsShiftDown()) { - selected = null; + Selected = null; } return; } @@ -91,24 +88,24 @@ namespace Barotrauma newRect = Submarine.AbsRect(placePosition, placeSize); } - sprite.DrawTiled(spriteBatch, new Vector2(newRect.X, -newRect.Y), new Vector2(newRect.Width, newRect.Height), textureScale: TextureScale * Scale); + Sprite.DrawTiled(spriteBatch, new Vector2(newRect.X, -newRect.Y), new Vector2(newRect.Width, newRect.Height), textureScale: TextureScale * Scale); GUI.DrawRectangle(spriteBatch, new Rectangle(newRect.X - GameMain.GraphicsWidth, -newRect.Y, newRect.Width + GameMain.GraphicsWidth * 2, newRect.Height), Color.White); GUI.DrawRectangle(spriteBatch, new Rectangle(newRect.X, -newRect.Y - GameMain.GraphicsHeight, newRect.Width, newRect.Height + GameMain.GraphicsHeight * 2), Color.White); } public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) { - SpriteEffects oldEffects = sprite.effects; - sprite.effects ^= spriteEffects; + SpriteEffects oldEffects = Sprite.effects; + Sprite.effects ^= spriteEffects; - sprite.DrawTiled( + Sprite.DrawTiled( spriteBatch, new Vector2(placeRect.X, -placeRect.Y), new Vector2(placeRect.Width, placeRect.Height), color: Color.White * 0.8f, textureScale: TextureScale * scale); - sprite.effects = oldEffects; + Sprite.effects = oldEffects; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 9f35f13be..90fb358ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -12,58 +12,9 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; -using System.Globalization; namespace Barotrauma { - class RoundSound - { - public Sound Sound; - public readonly float Volume; - public readonly float Range; - public readonly Vector2 FrequencyMultiplierRange; - public readonly bool Stream; - public readonly bool IgnoreMuffling; - - public readonly string Filename; - - public RoundSound(XElement element, Sound sound) - { - Filename = sound?.Filename; - Sound = sound; - Stream = sound.Stream; - Range = element.GetAttributeFloat("range", 1000.0f); - Volume = element.GetAttributeFloat("volume", 1.0f); - FrequencyMultiplierRange = new Vector2(1.0f); - string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0")); - if (!freqMultAttr.Contains(',')) - { - if (float.TryParse(freqMultAttr, NumberStyles.Any, CultureInfo.InvariantCulture, out float freqMult)) - { - FrequencyMultiplierRange = new Vector2(freqMult); - } - } - else - { - var freqMult = XMLExtensions.ParseVector2(freqMultAttr, false); - if (freqMult.Y >= 0.25f) - { - FrequencyMultiplierRange = freqMult; - } - } - if (FrequencyMultiplierRange.Y > 4.0f) - { - DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); - } - IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); - } - - public float GetRandomFrequencyMultiplier() - { - return Rand.Range(FrequencyMultiplierRange.X, FrequencyMultiplierRange.Y); - } - } - partial class Submarine : Entity, IServerSerializable { public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) @@ -82,97 +33,6 @@ namespace Barotrauma return worldGridPos; } - - private static List roundSounds = null; - public static RoundSound LoadRoundSound(XElement element, bool stream = false) - { - if (GameMain.SoundManager?.Disabled ?? true) { return null; } - - string filename = element.GetAttributeString("file", ""); - if (string.IsNullOrEmpty(filename)) filename = element.GetAttributeString("sound", ""); - - if (string.IsNullOrEmpty(filename)) - { - string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); - return null; - } - - filename = Path.GetFullPath(filename.CleanUpPath()).CleanUpPath(); - Sound existingSound = null; - if (roundSounds == null) - { - roundSounds = new List(); - } - else - { - existingSound = roundSounds.Find(s => s.Filename == filename && s.Stream == stream && !s.Sound.Disposed)?.Sound; - } - - if (existingSound == null) - { - try - { - existingSound = GameMain.SoundManager.LoadSound(filename, stream); - if (existingSound == null) { return null; } - } - catch (System.IO.FileNotFoundException e) - { - string errorMsg = "Failed to load sound file \"" + filename + "\"."; - DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); - return null; - } - } - - RoundSound newSound = new RoundSound(element, existingSound); - - roundSounds.Add(newSound); - return newSound; - } - - public static void ReloadRoundSound(RoundSound roundSound) - { - Sound existingSound = roundSounds?.Find(s => s.Filename == roundSound.Filename && s.Stream == roundSound.Stream && !s.Sound.Disposed)?.Sound; - if (existingSound == null) - { - try - { - existingSound = GameMain.SoundManager.LoadSound(roundSound.Filename, roundSound.Stream); - } - catch (System.IO.FileNotFoundException e) - { - string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; - DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } - } - roundSound.Sound = existingSound; - } - - private static void RemoveRoundSound(RoundSound roundSound) - { - roundSound.Sound?.Dispose(); - if (roundSounds == null) return; - - if (roundSounds.Contains(roundSound)) roundSounds.Remove(roundSound); - foreach (RoundSound otherSound in roundSounds) - { - if (otherSound.Sound == roundSound.Sound) otherSound.Sound = null; - } - } - - public static void RemoveAllRoundSounds() - { - if (roundSounds == null) return; - for (int i = roundSounds.Count - 1; i >= 0; i--) - { - RemoveRoundSound(roundSounds[i]); - } - } - //drawing ---------------------------------------------------- private static readonly HashSet visibleSubs = new HashSet(); public static void CullEntities(Camera cam) @@ -402,7 +262,7 @@ namespace Barotrauma var connectedSubs = GetConnectedSubs(); - HashSet hullList = Hull.hullList.Where(hull => hull.Submarine == this || connectedSubs.Contains(hull.Submarine)).Where(hull => !ignoreOutpost || IsEntityFoundOnThisSub(hull, true)).ToHashSet(); + HashSet hullList = Hull.HullList.Where(hull => hull.Submarine == this || connectedSubs.Contains(hull.Submarine)).Where(hull => !ignoreOutpost || IsEntityFoundOnThisSub(hull, true)).ToHashSet(); Dictionary> combinedHulls = new Dictionary>(); @@ -592,23 +452,23 @@ namespace Barotrauma List errorMsgs = new List(); List warnings = new List(); - if (!Hull.hullList.Any()) + if (!Hull.HullList.Any()) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints)) { - errorMsgs.Add(TextManager.Get("NoHullsWarning")); + errorMsgs.Add(TextManager.Get("NoHullsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoHulls); } } - if (Info.Type != SubmarineType.OutpostModule || - (Info.OutpostModuleInfo?.ModuleFlags.Any(f => !f.Equals("hallwayvertical", StringComparison.OrdinalIgnoreCase) && !f.Equals("hallwayhorizontal", StringComparison.OrdinalIgnoreCase)) ?? true)) + if (Info.Type != SubmarineType.OutpostModule || + (Info.OutpostModuleInfo?.ModuleFlags.Any(f => f != "hallwayvertical" && f != "hallwayhorizontal") ?? true)) { if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints)) { - errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); + errorMsgs.Add(TextManager.Get("NoWaypointsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoWaypoints); } } @@ -623,7 +483,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.DisconnectedVents)) { - errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning")); + errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning").Value); warnings.Add(SubEditorScreen.WarningType.DisconnectedVents); } break; @@ -634,7 +494,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHumanSpawnpoints)) { - errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoHumanSpawnpoints); } } @@ -642,7 +502,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoCargoSpawnpoints)) { - errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning")); + errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoCargoSpawnpoints); } } @@ -650,7 +510,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoBallastTag)) { - errorMsgs.Add(TextManager.Get("NoBallastTagsWarning")); + errorMsgs.Add(TextManager.Get("NoBallastTagsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoBallastTag); } } @@ -670,8 +530,8 @@ namespace Barotrauma if (doorLinks + wireCount > item.Connections[i].MaxWires) { errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", - new string[] { "[doorcount]", "[freeconnectioncount]" }, - new string[] { doorLinks.ToString(), (item.Connections[i].MaxWires - wireCount).ToString() })); + ("[doorcount]", doorLinks.ToString()), + ("[freeconnectioncount]", (item.Connections[i].MaxWires - wireCount).ToString())).Value); break; } } @@ -682,7 +542,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NonLinkedGaps)) { - errorMsgs.Add(TextManager.Get("NonLinkedGapsWarning")); + errorMsgs.Add(TextManager.Get("NonLinkedGapsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NonLinkedGaps); } } @@ -698,7 +558,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooManyLights)) { - errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning")); + errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning").Value); warnings.Add(SubEditorScreen.WarningType.TooManyLights); } } @@ -746,7 +606,7 @@ namespace Barotrauma var msgBox = new GUIMessageBox( TextManager.Get("Warning"), TextManager.Get("FarAwayEntitiesWarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked += (btn, obj) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index 2977533a7..c8e5cedeb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -83,31 +83,31 @@ namespace Barotrauma Spacing = 5 }; - ScalableFont font = parent.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; + GUIFont font = parent.Rect.Width < 350 ? GUIStyle.SmallFont : GUIStyle.Font; CreateSpecsWindow(descriptionBox, font, includeDescription: true); } - public void CreateSpecsWindow(GUIListBox parent, ScalableFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) + public void CreateSpecsWindow(GUIListBox parent, GUIFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) { float leftPanelWidth = 0.6f; float rightPanelWidth = 0.4f / leftPanelWidth; - string className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); + LocalizedString className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); - int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; + int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); GUITextBlock submarineNameText = null; GUITextBlock submarineClassText = null; if (includeTitle) { - int nameHeight = (int)GUI.LargeFont.MeasureString(DisplayName, true).Y; - submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; + int nameHeight = (int)GUIStyle.LargeFont.MeasureString(DisplayName, true).Y; + submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false }; submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); } if (includeClass) { - submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; + submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); } @@ -124,7 +124,7 @@ namespace Barotrauma Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; if (realWorldDimensions != Vector2.Zero) { - string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); + LocalizedString dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", ("[width]", ((int)realWorldDimensions.X).ToString()), ("[height]", ((int)realWorldDimensions.Y).ToString())); var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; @@ -134,7 +134,7 @@ namespace Barotrauma dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); } - string cargoCapacityStr = CargoCapacity < 0 ? TextManager.Get("unknown") : TextManager.GetWithVariables("cargocapacityformat", new string[1] { "[cratecount]" }, new string[1] {CargoCapacity.ToString() }); + var cargoCapacityStr = CargoCapacity < 0 ? TextManager.Get("unknown") : TextManager.GetWithVariable("cargocapacityformat", "[cratecount]", CargoCapacity.ToString()); var cargoCapacityText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("cargocapacity"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; @@ -200,11 +200,11 @@ namespace Barotrauma //space new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), parent.Content.RectTransform), style: null); - if (!string.IsNullOrEmpty(Description)) + if (!Description.IsNullOrEmpty()) { var wsItemDesc = new GUITextBlock(new RectTransform(new Vector2(1, 0), parent.Content.RectTransform), - TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) - { CanBeFocused = false, ForceUpperCase = true }; + TextManager.Get("SaveSubDialogDescription", "WorkshopItemDescription"), font: GUIStyle.Font, wrap: true) + { CanBeFocused = false, ForceUpperCase = ForceUpperCase.Yes }; descBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), parent.Content.RectTransform), Description, font: font, wrap: true) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 900a6d355..5aa7c9197 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -26,12 +26,12 @@ namespace Barotrauma private class HullCollection { public readonly List Rects; - public readonly string Name; + public readonly LocalizedString Name; - public HullCollection(string identifier) + public HullCollection(Identifier identifier) { Rects = new List(); - Name = TextManager.Get(identifier, returnNull: true) ?? identifier; + Name = TextManager.Get(identifier).Fallback(identifier.Value); } public void AddRect(XElement element) @@ -53,10 +53,9 @@ namespace Barotrauma } } - private readonly Dictionary hullCollections; + private readonly Dictionary hullCollections; private readonly List doors; - private static SubmarinePreview instance = null; public static void Create(SubmarineInfo submarineInfo) @@ -78,7 +77,7 @@ namespace Barotrauma isDisposed = false; loadTask = null; - hullCollections = new Dictionary(); + hullCollections = new Dictionary(); doors = new List(); previewFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); @@ -133,7 +132,7 @@ namespace Barotrauma }; var topLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 5f / 7f), topContainer.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft); - titleText = new GUITextBlock(new RectTransform(new Vector2(0.95f, 1f), topLayout.RectTransform), subInfo.DisplayName, font: GUI.LargeFont); + titleText = new GUITextBlock(new RectTransform(new Vector2(0.95f, 1f), topLayout.RectTransform), subInfo.DisplayName, font: GUIStyle.LargeFont); new GUIButton(new RectTransform(new Vector2(0.05f, 1f), topLayout.RectTransform), TextManager.Get("Close")) { OnClicked = (btn, obj) => { Dispose(); return false; } @@ -146,7 +145,7 @@ namespace Barotrauma ScrollBarVisible = false, Spacing = 5 }; - subInfo.CreateSpecsWindow(specsContainer, GUI.Font, includeTitle: false, includeDescription: true); + subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); int width = specsContainer.Rect.Width; void recalculateSpecsContainerHeight() { @@ -242,8 +241,8 @@ namespace Barotrauma BakeMapEntity(subElement); break; case "hull": - string identifier = subElement.GetAttributeString("roomname", "").ToLowerInvariant(); - if (!string.IsNullOrEmpty(identifier)) + Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); + if (!identifier.IsEmpty) { if (!hullCollections.TryGetValue(identifier, out HullCollection hullCollection)) { @@ -309,11 +308,11 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); if (prefab == null) { return; } - var texture = prefab.sprite.Texture; - var srcRect = prefab.sprite.SourceRect; + var texture = prefab.Sprite.Texture; + var srcRect = prefab.Sprite.SourceRect; SpriteEffects spriteEffects = SpriteEffects.None; if (flippedX && ((prefab as ItemPrefab)?.CanSpriteFlipX ?? true)) @@ -325,8 +324,8 @@ namespace Barotrauma spriteEffects |= SpriteEffects.FlipVertically; } - var prevEffects = prefab.sprite.effects; - prefab.sprite.effects ^= spriteEffects; + var prevEffects = prefab.Sprite.effects; + prefab.Sprite.effects ^= spriteEffects; bool overrideSprite = false; ItemPrefab itemPrefab = prefab as ItemPrefab; @@ -359,10 +358,10 @@ namespace Barotrauma if (flippedY) { textureOffset.Y = -textureOffset.Y; } backGroundOffset = new Vector2( - MathUtils.PositiveModulo((int)-textureOffset.X, prefab.sprite.SourceRect.Width), - MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.sprite.SourceRect.Height)); + MathUtils.PositiveModulo((int)-textureOffset.X, prefab.Sprite.SourceRect.Width), + MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.Sprite.SourceRect.Height)); - prefab.sprite.DrawTiled( + prefab.Sprite.DrawTiled( spriteRecorder, rect.Location.ToVector2() * new Vector2(1f, -1f), rect.Size.ToVector2(), @@ -385,17 +384,17 @@ namespace Barotrauma { if (!prefab.ResizeHorizontal) { - rect.Width = (int)(prefab.sprite.size.X * scale); + rect.Width = (int)(prefab.Sprite.size.X * scale); } if (!prefab.ResizeVertical) { - rect.Height = (int)(prefab.sprite.size.Y * scale); + rect.Height = (int)(prefab.Sprite.size.Y * scale); } var spritePos = rect.Center.ToVector2(); //spritePos.Y = rect.Height - spritePos.Y; - prefab.sprite.DrawTiled( + prefab.Sprite.DrawTiled( spriteRecorder, rect.Location.ToVector2() * new Vector2(1f, -1f), rect.Size.ToVector2(), @@ -413,7 +412,7 @@ namespace Barotrauma new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), rect.Size.ToVector2(), color: color, textureScale: Vector2.One * scale, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } else @@ -425,14 +424,14 @@ namespace Barotrauma spritePos.Y -= rect.Height; //spritePos.Y = rect.Height - spritePos.Y; - prefab.sprite.Draw( + prefab.Sprite.Draw( spriteRecorder, spritePos * new Vector2(1f, -1f), color, - prefab.sprite.Origin, + prefab.Sprite.Origin, rotation, scale, - prefab.sprite.effects, depth); + prefab.Sprite.effects, depth); foreach (var decorativeSprite in itemPrefab.DecorativeSprites) { @@ -442,14 +441,14 @@ namespace Barotrauma if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, - MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.sprite.effects, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } } } - prefab.sprite.effects = prevEffects; + prefab.Sprite.effects = prevEffects; } private void BakeItemComponents( @@ -467,7 +466,7 @@ namespace Barotrauma case "turret": Sprite barrelSprite = null; Sprite railSprite = null; - foreach (XElement turretSubElem in subElement.Elements()) + foreach (var turretSubElem in subElement.Elements()) { switch (turretSubElem.Name.ToString().ToLowerInvariant()) { @@ -494,13 +493,13 @@ namespace Barotrauma drawPos, color, rotation + MathHelper.PiOver2, scale, - SpriteEffects.None, depth + (railSprite.Depth - prefab.sprite.Depth)); + SpriteEffects.None, depth + (railSprite.Depth - prefab.Sprite.Depth)); barrelSprite?.Draw(spriteRecorder, drawPos, color, rotation + MathHelper.PiOver2, scale, - SpriteEffects.None, depth + (barrelSprite.Depth - prefab.sprite.Depth)); + SpriteEffects.None, depth + (barrelSprite.Depth - prefab.Sprite.Depth)); break; case "door": @@ -578,13 +577,13 @@ namespace Barotrauma if (!spriteRecorder.ReadyToRender) { - string waitText = !loadTask.IsCompleted ? - TextManager.Get("generatingsubmarinepreview", fallBackTag: "loading") : + LocalizedString waitText = !loadTask.IsCompleted ? + TextManager.Get("generatingsubmarinepreview", "loading") : (loadTask.Exception?.ToString() ?? "Task completed without marking as ready to render"); - Vector2 origin = (GUI.Font.MeasureString(waitText) * 0.5f); + Vector2 origin = (GUIStyle.Font.MeasureString(waitText) * 0.5f); origin.X = MathF.Round(origin.X); origin.Y = MathF.Round(origin.Y); - GUI.Font.DrawString( + GUIStyle.Font.DrawString( spriteBatch, waitText, scissorRectangle.Center.ToVector2(), @@ -629,18 +628,18 @@ namespace Barotrauma if (mouseOver) { - string str = hullCollection.Name; - Vector2 strSize = GUI.Font.MeasureString(str) / camera.Zoom; + LocalizedString str = hullCollection.Name; + Vector2 strSize = GUIStyle.Font.MeasureString(str) / camera.Zoom; Vector2 padding = new Vector2(30, 30) / camera.Zoom; Vector2 shift = new Vector2(10, 0) / camera.Zoom; GUI.DrawRectangle(spriteBatch, mousePos + shift, strSize + padding, Color.Black, isFilled: true, depth: 0.25f); - GUI.Font.DrawString(spriteBatch, str, mousePos + shift + (strSize + padding) * 0.5f, Color.White, 0f, strSize * camera.Zoom * 0.5f, 1f / camera.Zoom, SpriteEffects.None, 0f); + GUIStyle.Font.DrawString(spriteBatch, str, mousePos + shift + (strSize + padding) * 0.5f, Color.White, 0f, strSize * camera.Zoom * 0.5f, 1f / camera.Zoom, SpriteEffects.None, 0f); } } foreach (var door in doors) { - GUI.DrawRectangle(spriteBatch, door.Rect, GUI.Style.Green * 0.5f, isFilled: true, depth: 0.4f); + GUI.DrawRectangle(spriteBatch, door.Rect, GUIStyle.Green * 0.5f, isFilled: true, depth: 0.4f); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index b2cbce8ff..8cd2acaaf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -37,7 +37,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Vector2 drawPos) { - Color clr = CurrentHull == null ? Color.DodgerBlue : GUI.Style.Green; + Color clr = CurrentHull == null ? Color.DodgerBlue : GUIStyle.Green; if (spawnType != SpawnType.Path) { clr = Color.Gray; } if (isObstructed) { @@ -54,7 +54,7 @@ namespace Barotrauma if (IsSelected || IsHighlighted) { int glowSize = (int)(iconSize * 1.5f); - GUI.Style.UIGlowCircular.Draw(spriteBatch, + GUIStyle.UIGlowCircular.Draw(spriteBatch, new Rectangle((int)(drawPos.X - glowSize / 2), (int)(drawPos.Y - glowSize / 2), glowSize, glowSize), Color.White); } @@ -84,21 +84,21 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - (isObstructed ? Color.Gray : GUI.Style.Green) * 0.7f, width: 5, depth: 0.002f); + (isObstructed ? Color.Gray : GUIStyle.Green) * 0.7f, width: 5, depth: 0.002f); } if (ConnectedGap != null) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(ConnectedGap.DrawPosition.X, -ConnectedGap.DrawPosition.Y), - GUI.Style.Green * 0.5f, width: 1); + GUIStyle.Green * 0.5f, width: 1); } if (Ladders != null) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(Ladders.Item.DrawPosition.X, -Ladders.Item.DrawPosition.Y), - GUI.Style.Green * 0.5f, width: 1); + GUIStyle.Green * 0.5f, width: 1); } var color = Color.WhiteSmoke; @@ -123,13 +123,13 @@ namespace Barotrauma } } } - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, ID.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), color); if (Tunnel?.Type != null) { - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, Tunnel.Type.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45), color); @@ -289,13 +289,13 @@ namespace Barotrauma if (spawnType == SpawnType.Path) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Waypoint"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Waypoint"), font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("LinkWaypoint")); } else { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUI.LargeFont); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUIStyle.LargeFont); + var spawnTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true) { Stretch = true, @@ -318,8 +318,8 @@ namespace Barotrauma OnClicked = ChangeSpawnType }; - var descText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardDescription"), font: GUI.SmallFont) + var descText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), + TextManager.Get("IDCardDescription"), font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("IDCardDescriptionTooltip") }; @@ -336,17 +336,17 @@ namespace Barotrauma propertyBox.OnEnterPressed += (textBox, text) => { IdCardDesc = text; - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; }; propertyBox.OnDeselected += (textBox, keys) => { IdCardDesc = textBox.Text; - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); }; var idCardTagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardTags"), font: GUI.SmallFont) + TextManager.Get("IDCardTags"), font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("IDCardTagsTooltip") }; @@ -363,17 +363,17 @@ namespace Barotrauma propertyBox.OnEnterPressed += (textBox, text) => { textBox.Text = string.Join(",", IdCardTags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; }; propertyBox.OnDeselected += (textBox, keys) => { textBox.Text = string.Join(",", IdCardTags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); }; var jobsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("SpawnpointJobs"), font: GUI.SmallFont) + TextManager.Get("SpawnpointJobs"), font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("SpawnpointJobsTooltip") }; @@ -394,7 +394,7 @@ namespace Barotrauma jobDropDown.SelectItem(AssignedJob); var tagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("spawnpointtags"), font: GUI.SmallFont); + TextManager.Get("spawnpointtags"), font: GUIStyle.SmallFont); propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), tagsText.RectTransform, Anchor.CenterRight), string.Join(", ", tags)) { MaxTextLength = 60, @@ -402,19 +402,19 @@ namespace Barotrauma }; propertyBox.OnTextChanged += (textBox, text) => { - tags = text.Split(',').ToList(); + tags = text.Split(',').ToIdentifiers().ToHashSet(); return true; }; propertyBox.OnEnterPressed += (textBox, text) => { textBox.Text = string.Join(",", tags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; }; propertyBox.OnDeselected += (textBox, keys) => { textBox.Text = string.Join(",", tags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 898d39d8c..1e112d1df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -97,12 +97,12 @@ namespace Barotrauma.Networking new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), bannedPlayer.ExpirationTime == null ? TextManager.Get("BanPermanent") : TextManager.GetWithVariable("BanExpires", "[time]", bannedPlayer.ExpirationTime.Value.ToString()), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); var reasonText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), TextManager.Get("BanReason") + " " + (string.IsNullOrEmpty(bannedPlayer.Reason) ? TextManager.Get("None") : bannedPlayer.Reason), - font: GUI.SmallFont, wrap: true) + font: GUIStyle.SmallFont, wrap: true) { ToolTip = bannedPlayer.Reason }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 0e853822f..3a61415c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -61,25 +61,27 @@ namespace Barotrauma.Networking break; case ChatMessageType.Order: var orderMessageInfo = OrderChatMessage.ReadOrder(msg); - if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) + if (orderMessageInfo.OrderIdentifier == Identifier.Empty) { DebugConsole.ThrowError("Invalid order message - order index out of bounds."); if (NetIdUtils.IdMoreRecent(id, LastID)) { LastID = id; } return; } - var orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; - string orderOption = orderMessageInfo.OrderOption; - orderOption ??= orderMessageInfo.OrderOptionIndex.HasValue && orderMessageInfo.OrderOptionIndex >= 0 && orderMessageInfo.OrderOptionIndex < orderPrefab.Options.Length ? - orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value] : ""; + var orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; + Identifier orderOption = orderMessageInfo.OrderOption; + orderOption = orderOption.IfEmpty( + orderMessageInfo.OrderOptionIndex.HasValue && orderMessageInfo.OrderOptionIndex >= 0 && orderMessageInfo.OrderOptionIndex < orderPrefab.Options.Length + ? orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value] + : Identifier.Empty); string targetRoom; if (orderMessageInfo.TargetEntity is Hull targetHull) { - targetRoom = targetHull.DisplayName; + targetRoom = targetHull.DisplayName.Value; } else { - targetRoom = senderCharacter?.CurrentHull?.DisplayName; + targetRoom = senderCharacter?.CurrentHull?.DisplayName?.Value; } txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, targetRoom, @@ -93,18 +95,19 @@ namespace Barotrauma.Networking switch (orderMessageInfo.TargetType) { case Order.OrderTargetType.Entity: - order = new Order(orderPrefab, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: senderCharacter); break; case Order.OrderTargetType.Position: - order = new Order(orderPrefab, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); break; case Order.OrderTargetType.WallSection: - order = new Order(orderPrefab, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: senderCharacter); break; } if (order != null) { + order = order.WithManualPriority(orderMessageInfo.Priority); if (order.TargetAllCharacters) { var fadeOutTime = !orderPrefab.IsIgnoreOrder ? (float?)orderPrefab.FadeOutTime : null; @@ -112,24 +115,39 @@ namespace Barotrauma.Networking } else { - orderMessageInfo.TargetCharacter?.SetOrder(order, orderOption, orderMessageInfo.Priority, senderCharacter); + orderMessageInfo.TargetCharacter?.SetOrder(order); } } } if (NetIdUtils.IdMoreRecent(id, LastID)) { + Order order = null; + if (orderMessageInfo.TargetPosition != null) + { + order = new Order(orderPrefab, orderOption, orderMessageInfo.Priority, Order.OrderType.Current, null, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); + } + else if (orderMessageInfo.WallSectionIndex != null) + { + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: senderCharacter) + .WithManualPriority(orderMessageInfo.Priority); + } + else + { + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: senderCharacter) + .WithManualPriority(orderMessageInfo.Priority); + } GameMain.Client.AddChatMessage( - new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, txt, orderMessageInfo.TargetPosition ?? orderMessageInfo.TargetEntity as ISpatialEntity, orderMessageInfo.TargetCharacter, senderCharacter)); + new OrderChatMessage(order, txt, orderMessageInfo.TargetCharacter, senderCharacter)); LastID = id; } return; case ChatMessageType.ServerMessageBox: - txt = TextManager.GetServerMessage(txt); + txt = TextManager.GetServerMessage(txt).Value; break; case ChatMessageType.ServerMessageBoxInGame: styleSetting = msg.ReadString(); - txt = TextManager.GetServerMessage(txt); + txt = TextManager.GetServerMessage(txt).Value; break; } @@ -148,7 +166,7 @@ namespace Barotrauma.Networking break; case ChatMessageType.ServerMessageBoxInGame: { - GUIMessageBox messageBox = new GUIMessageBox("", txt, new string[0], type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); + GUIMessageBox messageBox = new GUIMessageBox("", txt, Array.Empty(), type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); if (textColor != null) { messageBox.Text.TextColor = textColor.Value; } } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index bd3111221..41ab884b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Networking struct TempClient { public string Name; - public string PreferredJob; + public Identifier PreferredJob; public CharacterTeamType PreferredTeam; public UInt16 NameID; public UInt64 SteamID; @@ -66,7 +66,7 @@ namespace Barotrauma.Networking if (character != null) { - if (GameMain.Config.UseDirectionalVoiceChat) + if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) { VoipSound.SetPosition(new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 86897d268..5fb9aac87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -19,7 +19,7 @@ namespace Barotrauma DebugConsole.Log($"Received entity removal message for \"{entity}\"."); if (entity is Item item && item.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); } entity.Remove(); } @@ -36,7 +36,7 @@ namespace Barotrauma var newItem = Item.ReadSpawnData(message, true); if (newItem is Item item && item.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); } break; case (byte)SpawnableType.Character: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 7fd0f8bb1..4c2721291 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.Linq; using System.Threading; @@ -35,6 +36,8 @@ namespace Barotrauma.Networking get; private set; } + + public int LastSeen { get; set; } public FileTransferType FileType { @@ -119,7 +122,7 @@ namespace Barotrauma.Networking int passed = Environment.TickCount - TimeStarted; float psec = passed / 1000.0f; - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log($"Received {all.Length} bytes of the file {FileName} ({Received / 1000}/{FileSize / 1000} kB received)"); } @@ -162,16 +165,15 @@ namespace Barotrauma.Networking private readonly List activeTransfers; private readonly List<(int transferId, double finishedTime)> finishedTransfers; - private readonly Dictionary downloadFolders = new Dictionary() + private readonly ImmutableDictionary downloadFolders = new Dictionary() { { FileTransferType.Submarine, SaveUtil.SubmarineDownloadFolder }, - { FileTransferType.CampaignSave, SaveUtil.CampaignDownloadFolder } - }; + { FileTransferType.CampaignSave, SaveUtil.CampaignDownloadFolder }, + { FileTransferType.Mod, ModReceiver.DownloadFolder } + }.ToImmutableDictionary(); - public List ActiveTransfers - { - get { return activeTransfers; } - } + public IReadOnlyList ActiveTransfers => activeTransfers; + public bool HasActiveTransfers => ActiveTransfers.Any(); public FileReceiver() { @@ -211,7 +213,7 @@ namespace Barotrauma.Networking } else //resend acknowledgement packet { - GameMain.Client.UpdateFileTransfer(transferId, existingTransfer.Received); + GameMain.Client.UpdateFileTransfer(transferId, existingTransfer.Received, existingTransfer.LastSeen); } return; } @@ -223,7 +225,7 @@ namespace Barotrauma.Networking return; } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log("Received file transfer initiation message: "); DebugConsole.Log(" File: " + fileName); @@ -278,7 +280,7 @@ namespace Barotrauma.Networking } activeTransfers.Add(newTransfer); - GameMain.Client.UpdateFileTransfer(transferId, 0); //send acknowledgement packet + GameMain.Client.UpdateFileTransfer(transferId, 0, 0); //send acknowledgement packet } break; case (byte)FileTransferMessageType.TransferOnSameMachine: @@ -287,7 +289,7 @@ namespace Barotrauma.Networking byte fileType = inc.ReadByte(); string filePath = inc.ReadString(); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log("Received file transfer message on the same machine: "); DebugConsole.Log(" File: " + filePath); @@ -308,7 +310,7 @@ namespace Barotrauma.Networking FileSize = 0 }; - Md5Hash.RemoveFromCache(directTransfer.FilePath); + Md5Hash.Cache.Remove(directTransfer.FilePath); OnFinished(directTransfer); } break; @@ -335,10 +337,12 @@ namespace Barotrauma.Networking int bytesToRead = inc.ReadUInt16(); if (offset != activeTransfer.Received) { + activeTransfer.LastSeen = Math.Max(offset, activeTransfer.LastSeen); DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} (ignoring: offset {offset}, waiting for {activeTransfer.Received})"); - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received); + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, activeTransfer.LastSeen); return; } + activeTransfer.LastSeen = offset; if (activeTransfer.Received + bytesToRead > activeTransfer.FileSize) { @@ -366,7 +370,7 @@ namespace Barotrauma.Networking return; } - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, reliable: activeTransfer.Status == FileTransferStatus.Finished); + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, activeTransfer.LastSeen, reliable: activeTransfer.Status == FileTransferStatus.Finished); if (activeTransfer.Status == FileTransferStatus.Finished) { activeTransfer.Dispose(); @@ -375,7 +379,7 @@ namespace Barotrauma.Networking { finishedTransfers.Add((transferId, Timing.TotalTime)); StopTransfer(activeTransfer); - Md5Hash.RemoveFromCache(activeTransfer.FilePath); + Md5Hash.Cache.Remove(activeTransfer.FilePath); OnFinished(activeTransfer); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/ModReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/ModReceiver.cs new file mode 100644 index 000000000..257c9b950 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/ModReceiver.cs @@ -0,0 +1,8 @@ +namespace Barotrauma.Networking +{ + static class ModReceiver + { + public const string DownloadFolder = "TempMods_Download"; + public const string Extension = ".barodir.gz"; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index ecc352288..23dd76fe6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -50,8 +51,9 @@ namespace Barotrauma.Networking private GUIMessageBox reconnectBox, waitInServerQueueBox; //TODO: move these to NetLobbyScreen + public LocalizedString endRoundVoteText; public GUITickBox EndVoteTickBox; - private GUIComponent buttonContainer; + private readonly GUIComponent buttonContainer; public readonly NetStats NetStats; @@ -121,7 +123,7 @@ namespace Barotrauma.Networking public bool HasSpawned; public bool SpawnAsTraitor; - public string TraitorFirstObjective; + public LocalizedString TraitorFirstObjective; public TraitorMissionPrefab TraitorMission = null; public byte ID @@ -221,13 +223,14 @@ namespace Barotrauma.Networking CanBeFocused = false }; + endRoundVoteText = TextManager.Get("EndRound"); EndVoteTickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.4f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) }, - TextManager.Get("EndRound")) + endRoundVoteText) { - UserData = TextManager.Get("EndRound"), OnSelected = ToggleEndRoundVote, Visible = false }; + EndVoteTickBox.TextBlock.Wrap = true; ShowLogButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.6f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) }, TextManager.Get("ServerLog")) @@ -282,8 +285,7 @@ namespace Barotrauma.Networking //ServerLog = new ServerLog(""); ChatMessage.LastID = 0; - GameMain.NetLobbyScreen?.Release(); - GameMain.NetLobbyScreen = new NetLobbyScreen(); + GameMain.ResetNetLobbyScreen(); } private void ConnectToServer(object endpoint, string hostName) @@ -340,7 +342,7 @@ namespace Barotrauma.Networking catch { new GUIMessageBox(TextManager.Get("CouldNotConnectToServer"), - TextManager.GetWithVariables("InvalidIPAddress", new string[2] { "[serverip]", "[port]" }, new string[2] { serverIP, port.ToString() })); + TextManager.GetWithVariables("InvalidIPAddress", ("[serverip]", serverIP), ("[port]", port.ToString()))); return; } @@ -361,39 +363,7 @@ namespace Barotrauma.Networking } clientPeer.OnDisconnect = OnDisconnect; clientPeer.OnDisconnectMessageReceived = HandleDisconnectMessage; - clientPeer.OnInitializationComplete = () => - { - if (SteamManager.IsInitialized) - { - Steamworks.SteamFriends.ClearRichPresence(); - Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + serverName); - Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"", "\\\"") + "\" " + serverEndpoint); - } - - canStart = true; - connected = true; - - VoipClient = new VoipClient(this, clientPeer); - - if (Screen.Selected != GameMain.GameScreen) - { - GameMain.NetLobbyScreen.Select(); - } - else - { - entityEventManager.ClearSelf(); - foreach (Character c in Character.CharacterList) - { - c.ResetNetState(); - } - } - - chatBox.InputBox.Enabled = true; - if (GameMain.NetLobbyScreen?.ChatInput != null) - { - GameMain.NetLobbyScreen.ChatInput.Enabled = true; - } - }; + clientPeer.OnInitializationComplete = OnConnectionInitializationComplete; clientPeer.OnRequestPassword = (int salt, int retries) => { if (pwRetries != retries) @@ -471,10 +441,10 @@ namespace Barotrauma.Networking canStart = false; DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 40); - DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); + DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); // Loop until we are approved - string connectingText = TextManager.Get("Connecting"); + LocalizedString connectingText = TextManager.Get("Connecting"); while (!canStart && !connectCancelled) { if (reconnectBox == null && waitInServerQueueBox == null) @@ -493,12 +463,12 @@ namespace Barotrauma.Networking } } } - if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown"); } + if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown").Value; } reconnectBox = new GUIMessageBox( connectingText, TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName), - new string[] { TextManager.Get("Cancel") }); + new LocalizedString[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; } @@ -524,9 +494,9 @@ namespace Barotrauma.Networking GUI.ClearCursorWait(); reconnectBox?.Close(); reconnectBox = null; - string pwMsg = TextManager.Get("PasswordRequired"); + LocalizedString pwMsg = TextManager.Get("PasswordRequired"); - var msgBox = new GUIMessageBox(pwMsg, "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, + var msgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) @@ -537,7 +507,7 @@ namespace Barotrauma.Networking if (wrongPassword) { - var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUI.Style.Red, GUI.Font, textAlignment: Alignment.Center); + var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center); incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y); passwordHolder.Recalculate(); } @@ -651,7 +621,7 @@ namespace Barotrauma.Networking } GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); DebugConsole.ThrowError("Error while reading a message from server.", e); - new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", new string[2] { "[message]", "[targetsite]" }, new string[2] { e.Message, e.TargetSite.ToString() })); + new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString()))); Disconnect(); GameMain.ServerListScreen.Select(); return; @@ -734,6 +704,22 @@ namespace Barotrauma.Networking MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; + if (Screen.Selected is ModDownloadScreen) + { + switch (header) + { + case ServerPacketHeader.UPDATE_LOBBY: + case ServerPacketHeader.PING_REQUEST: + case ServerPacketHeader.FILE_TRANSFER: + case ServerPacketHeader.PERMISSIONS: + case ServerPacketHeader.CHEATS_ENABLED: + //allow interpreting this packet + break; + default: + return; //ignore any other packets + } + } + switch (header) { case ServerPacketHeader.PING_REQUEST: @@ -980,9 +966,12 @@ namespace Barotrauma.Networking List contentToPreload = new List(); for (int i = 0; i < contentToPreloadCount; i++) { - ContentType contentType = (ContentType)inc.ReadByte(); string filePath = inc.ReadString(); - contentToPreload.Add(new ContentFile(filePath, contentType)); + ContentFile file = ContentPackageManager.EnabledPackages.All + .Select(p => + p.Files.FirstOrDefault(f => f.Path == filePath)) + .FirstOrDefault(f => !(f is null)); + contentToPreload.AddIfNotNull(file); } GameMain.GameSession.EventManager.PreloadContent(contentToPreload); @@ -1002,10 +991,10 @@ namespace Barotrauma.Networking string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})"; throw new Exception(errorMsg); } - List serverMissionIdentifiers = new List(); + List serverMissionIdentifiers = new List(); for (int i = 0; i < missionCount; i++) { - serverMissionIdentifiers.Add(inc.ReadString() ?? ""); + serverMissionIdentifiers.Add(inc.ReadIdentifier()); } if (missionCount > 0) @@ -1032,7 +1021,7 @@ namespace Barotrauma.Networking " (client value count: " + Level.Loaded.EqualityCheckValues.Count + ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); @@ -1048,7 +1037,7 @@ namespace Barotrauma.Networking ", server value #" + i + ": " + levelEqualityCheckValues[i].ToString("X") + ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); @@ -1076,7 +1065,7 @@ namespace Barotrauma.Networking reconnectBox?.Close(); reconnectBox = null; - GameMain.Config.RestoreBackupPackages(); + ContentPackageManager.EnabledPackages.Restore(); GUI.ClearCursorWait(); @@ -1138,7 +1127,7 @@ namespace Barotrauma.Networking var queueBox = new GUIMessageBox( TextManager.Get("DisconnectReason.ServerFull"), - TextManager.Get("ServerFullQuestionPrompt"), new string[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); + TextManager.Get("ServerFullQuestionPrompt"), new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); queueBox.Buttons[0].OnClicked += queueBox.Close; queueBox.Buttons[1].OnClicked += queueBox.Close; @@ -1178,15 +1167,15 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("Attempting to reconnect..."); //if the first part of the message is the disconnect reason Enum, don't include it in the popup message - string msg = TextManager.GetServerMessage(disconnectReasonIncluded ? string.Join('/', splitMsg.Skip(1)) : disconnectMsg); - msg = string.IsNullOrWhiteSpace(msg) ? + LocalizedString msg = TextManager.GetServerMessage(disconnectReasonIncluded ? string.Join('/', splitMsg.Skip(1)) : disconnectMsg); + msg = msg.IsNullOrWhiteSpace() ? TextManager.Get("ConnectionLostReconnecting") : msg + '\n' + TextManager.Get("ConnectionLostReconnecting"); reconnectBox?.Close(); reconnectBox = new GUIMessageBox( TextManager.Get("ConnectionLost"), msg, - new string[] { TextManager.Get("Cancel") }); + new LocalizedString[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; connected = false; ConnectToServer(serverEndpoint, serverName); @@ -1196,7 +1185,7 @@ namespace Barotrauma.Networking connected = false; connectCancelled = true; - string msg = ""; + LocalizedString msg = ""; if (disconnectReason == DisconnectReason.Unknown) { DebugConsole.NewMessage("Not attempting to reconnect (unknown disconnect reason)."); @@ -1241,11 +1230,45 @@ namespace Barotrauma.Networking } } + private void OnConnectionInitializationComplete() + { + if (SteamManager.IsInitialized) + { + Steamworks.SteamFriends.ClearRichPresence(); + Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + serverName); + Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"", "\\\"") + "\" " + serverEndpoint); + } + + canStart = true; + connected = true; + + VoipClient = new VoipClient(this, clientPeer); + + if (Screen.Selected != GameMain.GameScreen) + { + GameMain.ModDownloadScreen.Select(); + } + else + { + entityEventManager.ClearSelf(); + foreach (Character c in Character.CharacterList) + { + c.ResetNetState(); + } + } + + chatBox.InputBox.Enabled = true; + if (GameMain.NetLobbyScreen?.ChatInput != null) + { + GameMain.NetLobbyScreen.ChatInput.Enabled = true; + } + } + private IEnumerable WaitInServerQueue() { waitInServerQueueBox = new GUIMessageBox( TextManager.Get("ServerQueuePleaseWait"), - TextManager.Get("WaitingInServerQueue"), new string[] { TextManager.Get("Cancel") }); + TextManager.Get("WaitingInServerQueue"), new LocalizedString[] { TextManager.Get("Cancel") }); waitInServerQueueBox.Buttons[0].OnClicked += (btn, userdata) => { CoroutineManager.StopCoroutines("WaitInServerQueue"); @@ -1273,7 +1296,7 @@ namespace Barotrauma.Networking private void ReadAchievement(IReadMessage inc) { - string achievementIdentifier = inc.ReadString(); + Identifier achievementIdentifier = inc.ReadIdentifier(); int amount = inc.ReadInt32(); if (amount == 0) { @@ -1289,16 +1312,16 @@ namespace Barotrauma.Networking { TraitorMessageType messageType = (TraitorMessageType)inc.ReadByte(); string missionIdentifier = inc.ReadString(); - string message = inc.ReadString(); - message = TextManager.GetServerMessage(message); + string messageFmt = inc.ReadString(); + LocalizedString message = TextManager.GetServerMessage(messageFmt); - var missionPrefab = TraitorMissionPrefab.List.Find(t => t.Identifier == missionIdentifier); + var missionPrefab = TraitorMissionPrefab.Prefabs.Find(t => t.Identifier == missionIdentifier); Sprite icon = missionPrefab?.Icon; switch (messageType) { case TraitorMessageType.Objective: - var isTraitor = !string.IsNullOrEmpty(message); + var isTraitor = !message.IsNullOrEmpty(); SpawnAsTraitor = isTraitor; TraitorFirstObjective = message; TraitorMission = missionPrefab; @@ -1309,11 +1332,11 @@ namespace Barotrauma.Networking } break; case TraitorMessageType.Console: - GameMain.Client.AddChatMessage(ChatMessage.Create("", message, ChatMessageType.Console, null)); + GameMain.Client.AddChatMessage(ChatMessage.Create("", message.Value, ChatMessageType.Console, null)); DebugConsole.NewMessage(message); break; case TraitorMessageType.ServerMessageBox: - var msgBox = new GUIMessageBox("", message, new string[0], type: GUIMessageBox.Type.InGame, icon: icon); + var msgBox = new GUIMessageBox("", message, Array.Empty(), type: GUIMessageBox.Type.InGame, icon: icon); if (msgBox.Icon != null) { msgBox.IconColor = missionPrefab.IconColor; @@ -1321,7 +1344,7 @@ namespace Barotrauma.Networking break; case TraitorMessageType.Server: default: - GameMain.Client.AddChatMessage(message, ChatMessageType.Server); + GameMain.Client.AddChatMessage(message.Value, ChatMessageType.Server); break; } } @@ -1369,7 +1392,7 @@ namespace Barotrauma.Networking msgBox.Content.ClearChildren(); msgBox.Content.RectTransform.RelativeSize = new Vector2(0.95f, 0.9f); - var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgBox.Content.RectTransform), TextManager.Get("PermissionsChanged"), textAlignment: Alignment.Center, font: GUI.LargeFont); + var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgBox.Content.RectTransform), TextManager.Get("PermissionsChanged"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont); header.RectTransform.IsFixedSize = true; var permissionArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; @@ -1378,12 +1401,12 @@ namespace Barotrauma.Networking var permissionsLabel = new GUITextBlock(new RectTransform(new Vector2(newPermissions == ClientPermissions.None ? 2.0f : 1.0f, 0.0f), leftColumn.RectTransform), TextManager.Get(newPermissions == ClientPermissions.None ? "PermissionsRemoved" : "CurrentPermissions"), - wrap: true, font: (newPermissions == ClientPermissions.None ? GUI.Font : GUI.SubHeadingFont)); + wrap: true, font: (newPermissions == ClientPermissions.None ? GUIStyle.Font : GUIStyle.SubHeadingFont)); permissionsLabel.RectTransform.NonScaledSize = new Point(permissionsLabel.Rect.Width, permissionsLabel.Rect.Height); permissionsLabel.RectTransform.IsFixedSize = true; if (newPermissions != ClientPermissions.None) { - string permissionList = ""; + LocalizedString permissionList = ""; foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { if (!newPermissions.HasFlag(permission) || permission == ClientPermissions.None) { continue; } @@ -1396,12 +1419,12 @@ namespace Barotrauma.Networking if (newPermissions.HasFlag(ClientPermissions.ConsoleCommands)) { var commandsLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), - TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUI.SubHeadingFont); + TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); var commandList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)); foreach (string permittedCommand in permittedConsoleCommands) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), commandList.Content.RectTransform, minSize: new Point(0, 15)), - permittedCommand, font: GUI.SmallFont) + permittedCommand, font: GUIStyle.SmallFont) { CanBeFocused = false }; @@ -1504,11 +1527,11 @@ namespace Barotrauma.Networking string subHash = inc.ReadString(); string shuttleName = inc.ReadString(); string shuttleHash = inc.ReadString(); - List missionIndices = new List(); + List missionHashes = new List(); int missionCount = inc.ReadByte(); for (int i = 0; i < missionCount; i++) { - missionIndices.Add(inc.ReadInt16()); + missionHashes.Add(inc.ReadUInt32()); } if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList)) { @@ -1525,7 +1548,7 @@ namespace Barotrauma.Networking //this shouldn't happen, TrySelectSub should stop the coroutine if the correct sub/shuttle cannot be found if (GameMain.NetLobbyScreen.SelectedSub == null || GameMain.NetLobbyScreen.SelectedSub.Name != subName || - GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash != subHash) + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash) { string errorMsg = "Failed to select submarine \"" + subName + "\" (hash: " + subHash + ")."; if (GameMain.NetLobbyScreen.SelectedSub == null) @@ -1538,9 +1561,9 @@ namespace Barotrauma.Networking { errorMsg += "\n" + "Name mismatch: " + GameMain.NetLobbyScreen.SelectedSub.Name + " != " + subName; } - if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash != subHash) + if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash) { - errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash + " != " + subHash; + errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation + " != " + subHash; } } gameStarted = true; @@ -1552,7 +1575,7 @@ namespace Barotrauma.Networking } if (GameMain.NetLobbyScreen.SelectedShuttle == null || GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName || - GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.Hash != shuttleHash) + GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.StringRepresentation != shuttleHash) { gameStarted = true; GameMain.NetLobbyScreen.Select(); @@ -1563,7 +1586,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - var selectedMissions = missionIndices.Select(i => MissionPrefab.List[i]); + var selectedMissions = missionHashes.Select(i => MissionPrefab.Prefabs.Find(p => p.UintIdentifier == i)); GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefabs: selectedMissions); GameMain.GameSession.StartRound(levelSeed, levelDifficulty); @@ -1767,7 +1790,7 @@ namespace Barotrauma.Networking GameMain.GameScreen.Select(); - AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameMain.Config.KeyBindText(InputType.Chat)}~[radiobutton]={GameMain.Config.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); + AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Chat)}~[radiobutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); yield return CoroutineStatus.Success; } @@ -1850,7 +1873,7 @@ namespace Barotrauma.Networking byte subClass = inc.ReadByte(); bool requiredContentPackagesInstalled = inc.ReadBoolean(); - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); if (matchingSub == null) { matchingSub = new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false) @@ -1913,7 +1936,7 @@ namespace Barotrauma.Networking if (Screen.Selected != GameMain.GameScreen) { new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled")); - GameMain.NetLobbyScreen.Select(); + if (!(Screen.Selected is ModDownloadScreen)) { GameMain.NetLobbyScreen.Select(); } } } } @@ -1930,7 +1953,7 @@ namespace Barotrauma.Networking UInt64 steamId = inc.ReadUInt64(); UInt16 nameId = inc.ReadUInt16(); string name = inc.ReadString(); - string preferredJob = inc.ReadString(); + Identifier preferredJob = inc.ReadIdentifier(); byte preferredTeam = inc.ReadByte(); UInt16 characterID = inc.ReadUInt16(); float karma = inc.ReadSingle(); @@ -2088,7 +2111,7 @@ namespace Barotrauma.Networking bool isInitialUpdate = inc.ReadBoolean(); if (isInitialUpdate) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Received initial lobby update, ID: " + updateID + ", last ID: " + GameMain.NetLobbyScreen.LastUpdateID, Color.Gray); } @@ -2253,7 +2276,10 @@ namespace Barotrauma.Networking } break; case ServerNetObject.ENTITY_POSITION: - bool isItem = inc.ReadBoolean(); + inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + + bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); + UInt32 incomingUintIdentifier = inc.ReadUInt32(); UInt16 id = inc.ReadUInt16(); uint msgLength = inc.ReadVariableUInt32(); int msgEndPos = (int)(inc.BitPosition + msgLength * 8); @@ -2272,11 +2298,18 @@ namespace Barotrauma.Networking { DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); } + else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && + uintIdentifier != incomingUintIdentifier) + { + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." + +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " + +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); + } else { entity.ClientRead(objHeader.Value, inc, sendingTime); } - } + } //force to the correct position in case the entity doesn't exist //or the message wasn't read correctly for whatever reason @@ -2382,13 +2415,13 @@ namespace Barotrauma.Networking var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; if (jobPreferences.Count > 0) { - outmsg.Write(jobPreferences[0].First.Identifier); + outmsg.Write(jobPreferences[0].Prefab.Identifier); } else { outmsg.Write(""); } - outmsg.Write((byte)GameMain.Config.TeamPreference); + outmsg.Write((byte)MultiplayerPreferences.Instance.TeamPreference); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { @@ -2512,8 +2545,8 @@ namespace Barotrauma.Networking msg.Write((byte)ClientPacketHeader.FILE_REQUEST); msg.Write((byte)FileTransferMessageType.Initiate); msg.Write((byte)fileType); - if (file != null) msg.Write(file); - if (fileHash != null) msg.Write(fileHash); + msg.Write(file ?? throw new ArgumentNullException(nameof(file))); + msg.Write(fileHash ?? throw new ArgumentNullException(nameof(fileHash))); clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2522,13 +2555,14 @@ namespace Barotrauma.Networking CancelFileTransfer(transfer.ID); } - public void UpdateFileTransfer(int id, int offset, bool reliable = false) + public void UpdateFileTransfer(int id, int expecting, int lastSeen, bool reliable = false) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.FILE_REQUEST); msg.Write((byte)FileTransferMessageType.Data); msg.Write((byte)id); - msg.Write(offset); + msg.Write(expecting); + msg.Write(lastSeen); clientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); } @@ -2550,7 +2584,7 @@ namespace Barotrauma.Networking var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } - var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.Hash == newSub.MD5Hash.Hash).ToList(); + var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation).ToList(); foreach (SubmarineInfo existingSub in existingSubs) { existingSub.Dispose(); @@ -2565,7 +2599,7 @@ namespace Barotrauma.Networking var subElement = subListChildren.FirstOrDefault(c => ((SubmarineInfo)c.UserData).Name == newSub.Name && - ((SubmarineInfo)c.UserData).MD5Hash.Hash == newSub.MD5Hash.Hash); + ((SubmarineInfo)c.UserData).MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation); if (subElement == null) continue; Color newSubTextColor = new Color(subElement.GetChild().TextColor, 1.0f); @@ -2585,25 +2619,25 @@ namespace Barotrauma.Networking if (GameMain.NetLobbyScreen.FailedSelectedSub.HasValue && GameMain.NetLobbyScreen.FailedSelectedSub.Value.Name == newSub.Name && - GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.Hash) + GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.StringRepresentation) { - GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.Hash, GameMain.NetLobbyScreen.SubList); + GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.SubList); } if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue && GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name && - GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.Hash) + GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.MD5Hash.StringRepresentation) { - GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.Hash, GameMain.NetLobbyScreen.ShuttleList.ListBox); + GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.ShuttleList.ListBox); } - NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); + NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation); if (failedCampaignSub != default) { GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub); } - NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); + NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation); if (failedOwnedSub != default) { GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(newSub); @@ -2611,7 +2645,7 @@ namespace Barotrauma.Networking } // Replace a submarine dud with the downloaded version - SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.Hash == newSub.MD5Hash?.Hash); + SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.StringRepresentation == newSub.MD5Hash?.StringRepresentation); if (existingServerSub != null) { int existingIndex = ServerSubmarines.IndexOf(existingServerSub); @@ -2661,6 +2695,11 @@ namespace Barotrauma.Networking //(as there may have been campaign updates after the save file was created) campaign.LastUpdateID--; break; + case FileTransferType.Mod: + if (!(Screen.Selected is ModDownloadScreen)) { return; } + + GameMain.ModDownloadScreen.CurrentDownloadFinished(transfer); + break; } } @@ -2756,24 +2795,26 @@ namespace Barotrauma.Networking msg.Write(characterInfo == null); if (characterInfo == null) return; - msg.Write((byte)characterInfo.Gender); - msg.Write((byte)characterInfo.Race); - msg.Write((byte)characterInfo.HeadSpriteId); - msg.Write((byte)characterInfo.HairIndex); - msg.Write((byte)characterInfo.BeardIndex); - msg.Write((byte)characterInfo.MoustacheIndex); - msg.Write((byte)characterInfo.FaceAttachmentIndex); - msg.WriteColorR8G8B8(characterInfo.SkinColor); - msg.WriteColorR8G8B8(characterInfo.HairColor); - msg.WriteColorR8G8B8(characterInfo.FacialHairColor); + msg.Write((byte)characterInfo.Head.Preset.TagSet.Count); + foreach (Identifier tag in characterInfo.Head.Preset.TagSet) + { + msg.Write(tag); + } + msg.Write((byte)characterInfo.Head.HairIndex); + msg.Write((byte)characterInfo.Head.BeardIndex); + msg.Write((byte)characterInfo.Head.MoustacheIndex); + msg.Write((byte)characterInfo.Head.FaceAttachmentIndex); + msg.WriteColorR8G8B8(characterInfo.Head.SkinColor); + msg.WriteColorR8G8B8(characterInfo.Head.HairColor); + msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; int count = Math.Min(jobPreferences.Count, 3); msg.Write((byte)count); for (int i = 0; i < count; i++) { - msg.Write(jobPreferences[i].First.Identifier); - msg.Write((byte)jobPreferences[i].Second); + msg.Write(jobPreferences[i].Prefab.Identifier); + msg.Write((byte)jobPreferences[i].Variant); } } @@ -2976,7 +3017,7 @@ namespace Barotrauma.Networking msg.Write(saveName); msg.Write(mapSeed); msg.Write(sub.Name); - msg.Write(sub.MD5Hash.Hash); + msg.Write(sub.MD5Hash.StringRepresentation); settings.Serialize(msg); clientPeer.Send(msg, DeliveryMethod.Reliable); @@ -3259,7 +3300,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.FileTransferFrame.UserData = transfer; GameMain.NetLobbyScreen.FileTransferTitle.Text = ToolBox.LimitString( - TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName), + TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName).Value, GameMain.NetLobbyScreen.FileTransferTitle.Font, GameMain.NetLobbyScreen.FileTransferTitle.Rect.Width); GameMain.NetLobbyScreen.FileTransferProgressBar.BarSize = transfer.Progress; @@ -3279,26 +3320,25 @@ namespace Barotrauma.Networking { if (EndVoteTickBox.Visible) { - EndVoteTickBox.Text = - (EndVoteTickBox.UserData as string) + " " + EndVoteCount + "/" + EndVoteMax; + EndVoteTickBox.Text = $"{endRoundVoteText} {EndVoteCount}/{EndVoteMax}"; } else { - string endVoteText = TextManager.GetWithVariables("EndRoundVotes", new string[2] { "[votes]", "[max]" }, new string[2] { EndVoteCount.ToString(), EndVoteMax.ToString() }); - GUI.DrawString(spriteBatch, EndVoteTickBox.Rect.Center.ToVector2() - GUI.SmallFont.MeasureString(endVoteText) / 2, - endVoteText, + LocalizedString endVoteText = TextManager.GetWithVariables("EndRoundVotes", ("[votes]", EndVoteCount.ToString()), ("[max]", EndVoteMax.ToString())); + GUI.DrawString(spriteBatch, EndVoteTickBox.Rect.Center.ToVector2() - GUIStyle.SmallFont.MeasureString(endVoteText) / 2, + endVoteText.Value, Color.White, - font: GUI.SmallFont); + font: GUIStyle.SmallFont); } } else { - EndVoteTickBox.Text = EndVoteTickBox.UserData as string; + EndVoteTickBox.Text = endRoundVoteText; } if (respawnManager != null) { - string respawnText = string.Empty; + LocalizedString respawnText = string.Empty; Color textColor = Color.White; bool canChooseRespawn = GameMain.GameSession.GameMode is CampaignMode && @@ -3316,9 +3356,9 @@ namespace Barotrauma.Networking } else if (respawnManager.PendingRespawnCount > 0) { - respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers", - new string[] { "[deadplayers]", "[requireddeadplayers]" }, - new string[] { respawnManager.PendingRespawnCount.ToString(), respawnManager.RequiredRespawnCount.ToString() }); + respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers", + ("[deadplayers]", respawnManager.PendingRespawnCount.ToString()), + ("[requireddeadplayers]", respawnManager.RequiredRespawnCount.ToString())); } } else if (respawnManager.CurrentState == RespawnManager.State.Transporting && @@ -3333,13 +3373,13 @@ namespace Barotrauma.Networking //oscillate between 0-1 float phase = (float)(Math.Sin(timeLeft * MathHelper.Pi) + 1.0f) * 0.5f; //textScale = 1.0f + phase * 0.5f; - textColor = Color.Lerp(GUI.Style.Red, Color.White, 1.0f - phase); + textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase); } canChooseRespawn = false; } GameMain.GameSession?.SetRespawnInfo( - visible: !string.IsNullOrEmpty(respawnText) || canChooseRespawn, text: respawnText, textColor: textColor, + visible: !respawnText.IsNullOrEmpty() || canChooseRespawn, text: respawnText.Value, textColor: textColor, buttonsVisible: canChooseRespawn, waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true)); } @@ -3352,23 +3392,23 @@ namespace Barotrauma.Networking int x = GameMain.GraphicsWidth - width, y = (int)(GameMain.GraphicsHeight * 0.3f); GUI.DrawRectangle(spriteBatch, new Rectangle(x, y, width, height), Color.Black * 0.7f, true); - GUI.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White); + GUIStyle.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White); if (client.ServerConnection != null) { - GUI.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White); + GUIStyle.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White); y += 15; - GUI.SmallFont.DrawString(spriteBatch, "Received bytes: " + client.Statistics.ReceivedBytes, new Vector2(x + 10, y + 45), Color.White); - GUI.SmallFont.DrawString(spriteBatch, "Received packets: " + client.Statistics.ReceivedPackets, new Vector2(x + 10, y + 60), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Received bytes: " + client.Statistics.ReceivedBytes, new Vector2(x + 10, y + 45), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Received packets: " + client.Statistics.ReceivedPackets, new Vector2(x + 10, y + 60), Color.White); - GUI.SmallFont.DrawString(spriteBatch, "Sent bytes: " + client.Statistics.SentBytes, new Vector2(x + 10, y + 75), Color.White); - GUI.SmallFont.DrawString(spriteBatch, "Sent packets: " + client.Statistics.SentPackets, new Vector2(x + 10, y + 90), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Sent bytes: " + client.Statistics.SentBytes, new Vector2(x + 10, y + 75), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Sent packets: " + client.Statistics.SentPackets, new Vector2(x + 10, y + 90), Color.White); } else { - GUI.Font.DrawString(spriteBatch, "Disconnected", new Vector2(x + 10, y + 25), Color.White); + GUIStyle.Font.DrawString(spriteBatch, "Disconnected", new Vector2(x + 10, y + 25), Color.White); }*/ } @@ -3471,7 +3511,7 @@ namespace Barotrauma.Networking { var banReasonPrompt = new GUIMessageBox( TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), - "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.25f), new Point(400, 260)); + "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.25f), new Point(400, 260)); var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)) { @@ -3489,7 +3529,7 @@ namespace Barotrauma.Networking if (ban) { var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), content.RectTransform), isHorizontal: false); - new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get("BanDuration"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get("BanDuration"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; var buttonContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal: true); permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get("BanPermanent")) { @@ -3599,7 +3639,7 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode != null) { - errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name); + errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name.Value); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { errorLines.Add("Campaign ID: " + campaign.CampaignID); @@ -3623,19 +3663,18 @@ namespace Barotrauma.Networking errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); - foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate) + foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) { - errorLines.Add(" " + e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate); } errorLines.Add("Entity IDs:"); - List sortedEntities = Entity.GetEntities().ToList(); - sortedEntities.Sort((e1, e2) => e1.ID.CompareTo(e2.ID)); + Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray(); foreach (Entity e in sortedEntities) { - errorLines.Add(e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add(""); @@ -3645,7 +3684,7 @@ namespace Barotrauma.Networking errorLines.Add(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text); } - string filePath = "event_error_log_client_" + Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + string filePath = $"event_error_log_client_{Name}_{DateTime.UtcNow.ToShortTimeString()}.log"; filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (!Directory.Exists(ServerLog.SavePath)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index e53215e38..e26337d66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -28,7 +28,7 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 50.0f, 1.0f, nameof(KarmaIncreaseThreshold)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.12f), parent.RectTransform), TextManager.Get("Karma.PositiveActions"), - textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; @@ -41,7 +41,7 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(BallastFloraKarmaIncrease)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.12f), parent.RectTransform), TextManager.Get("Karma.NegativeActions"), - textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; @@ -86,9 +86,9 @@ namespace Barotrauma ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; - string labelText = TextManager.Get("Karma." + propertyName); + LocalizedString labelText = TextManager.Get("Karma." + propertyName); var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - labelText, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont) + labelText, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; @@ -120,8 +120,8 @@ namespace Barotrauma ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; - string labelText = TextManager.Get("Karma." + propertyName); - new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), labelText, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont) + LocalizedString labelText = TextManager.Get("Karma." + propertyName); + new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), labelText, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; @@ -140,7 +140,7 @@ namespace Barotrauma { var tickBox = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.1f), parent.RectTransform), TextManager.Get("Karma." + propertyName)) { - ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip", returnNull: true) ?? "" + ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip").Fallback("") }; GameMain.NetworkMember.ServerSettings.AssignGUIComponent(propertyName, tickBox); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 893cfe2e2..0d67a7e1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -120,7 +120,7 @@ namespace Barotrauma.Networking unreceivedEntityEventCount = msg.ReadUInt16(); firstNewID = msg.ReadUInt16(); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage( "received midround syncing msg, unreceived: " + unreceivedEntityEventCount + @@ -132,7 +132,7 @@ namespace Barotrauma.Networking MidRoundSyncingDone = true; if (firstNewID != null) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("midround syncing complete, switching to ID " + (UInt16) (firstNewID - 1), Microsoft.Xna.Framework.Color.Yellow); @@ -167,7 +167,7 @@ namespace Barotrauma.Networking if (entityID == Entity.NullEntityID) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", Microsoft.Xna.Framework.Color.Orange); @@ -188,12 +188,12 @@ namespace Barotrauma.Networking { if (thisEventID != (UInt16) (lastReceivedID + 1)) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage( "Received msg " + thisEventID + " (waiting for " + (lastReceivedID + 1) + ")", NetIdUtils.IdMoreRecent(thisEventID, (UInt16)(lastReceivedID + 1)) - ? GUI.Style.Red + ? GUIStyle.Red : Microsoft.Xna.Framework.Color.Yellow); } } @@ -201,7 +201,7 @@ namespace Barotrauma.Networking { DebugConsole.NewMessage( "Received msg " + thisEventID + ", entity " + entityID + " not found", - GUI.Style.Red); + GUIStyle.Red); GameMain.Client.ReportError(ClientNetError.MISSING_ENTITY, eventID: thisEventID, entityID: entityID); return false; } @@ -212,7 +212,7 @@ namespace Barotrauma.Networking else { int msgPosition = msg.BitPosition; - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (" + entity.ToString() + ")", Microsoft.Xna.Framework.Color.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs index e77762455..8604fcf9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs @@ -61,29 +61,29 @@ namespace Barotrauma.Networking GUI.DrawRectangle(spriteBatch, rect, Color.Black * 0.4f, true); graphs[(int)NetStatType.ReceivedBytes].Draw(spriteBatch, rect, color: Color.Cyan); - graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, color: GUI.Style.Orange); + graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, color: GUIStyle.Orange); if (graphs[(int)NetStatType.ResentMessages].Average() > 0) { - graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, color: GUI.Style.Red); - GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", - new Vector2(rect.Right + 10, rect.Y + 50), GUI.Style.Red); + graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, color: GUIStyle.Red); + GUIStyle.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", + new Vector2(rect.Right + 10, rect.Y + 50), GUIStyle.Red); } - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, "Peak received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].LargestValue()) + "/s " + "Avg received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].Average()) + "/s", new Vector2(rect.Right + 10, rect.Y + 10), Color.Cyan); - GUI.SmallFont.DrawString(spriteBatch, "Peak sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].LargestValue()) + "/s " + + GUIStyle.SmallFont.DrawString(spriteBatch, "Peak sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].LargestValue()) + "/s " + "Avg sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].Average()) + "/s", - new Vector2(rect.Right + 10, rect.Y + 30), GUI.Style.Orange); + new Vector2(rect.Right + 10, rect.Y + 30), GUIStyle.Orange); #if DEBUG /*int y = 10; foreach (KeyValuePair msgBytesSent in server.messageCount.OrderBy(key => -key.Value)) { - GUI.SmallFont.DrawString(spriteBatch, msgBytesSent.Key + ": " + MathUtils.GetBytesReadable(msgBytesSent.Value), - new Vector2(rect.Right - 200, rect.Y + y), GUI.Style.Red); + GUIStyle.SmallFont.DrawString(spriteBatch, msgBytesSent.Key + ": " + MathUtils.GetBytesReadable(msgBytesSent.Value), + new Vector2(rect.Right - 200, rect.Y + y), GUIStyle.Red); y += 15; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index cadf2a25b..3134ee4f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -2,6 +2,7 @@ using Barotrauma.Steam; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Text; @@ -10,30 +11,37 @@ namespace Barotrauma.Networking { abstract class ClientPeer { - protected class ServerContentPackage + public class ServerContentPackage { public readonly string Name; - public readonly string Hash; + public readonly Md5Hash Hash; public readonly UInt64 WorkshopId; public readonly DateTime InstallTime; - public ContentPackage RegularPackage + public RegularPackage RegularPackage { get { - return ContentPackage.RegularPackages.Find(p => p.MD5hash.Hash.Equals(Hash)); + return ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Hash.Equals(Hash)); } } - public ContentPackage CorePackage + public CorePackage CorePackage { get { - return ContentPackage.CorePackages.Find(p => p.MD5hash.Hash.Equals(Hash)); + return ContentPackageManager.CorePackages.FirstOrDefault(p => p.Hash.Equals(Hash)); } } - public ServerContentPackage(string name, string hash, UInt64 workshopId, DateTime installTime) + public ContentPackage ContentPackage + => (ContentPackage)RegularPackage ?? CorePackage; + + + public string GetPackageStr() + => $"\"{Name}\" (hash {Hash.ShortRepresentation})"; + + public ServerContentPackage(string name, Md5Hash hash, UInt64 workshopId, DateTime installTime) { Name = name; Hash = hash; @@ -42,14 +50,8 @@ namespace Barotrauma.Networking } } - protected string GetPackageStr(ContentPackage contentPackage) - { - return $"\"{contentPackage.Name}\" (hash {contentPackage.MD5hash.ShortHash})"; - } - protected string GetPackageStr(ServerContentPackage contentPackage) - { - return $"\"{contentPackage.Name}\" (hash {Md5Hash.GetShortHash(contentPackage.Hash)})"; - } + public ImmutableArray ServerContentPackages { get; private set; } = + ImmutableArray.Empty; public delegate void MessageCallback(IReadMessage message); public delegate void DisconnectCallback(bool disableReconnect); @@ -72,7 +74,7 @@ namespace Barotrauma.Networking public abstract void Start(object endPoint, int ownerKey); public abstract void Close(string msg = null, bool disableReconnect = false); public abstract void Update(float deltaTime); - public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod); + public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void SendPassword(string password); protected abstract void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg); @@ -108,7 +110,7 @@ namespace Barotrauma.Networking outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); } outMsg.Write(GameMain.Version.ToString()); - outMsg.Write(GameMain.Config.Language); + outMsg.Write(GameSettings.CurrentConfig.Language.Value); SendMsgInternal(DeliveryMethod.Reliable, outMsg); break; @@ -122,121 +124,26 @@ namespace Barotrauma.Networking string serverName = inc.ReadString(); - UInt32 cpCount = inc.ReadVariableUInt32(); - ServerContentPackage corePackage = null; - List regularPackages = new List(); - List missingPackages = new List(); - for (int i = 0; i < cpCount; i++) + UInt32 packageCount = inc.ReadVariableUInt32(); + List serverPackages = new List(); + for (int i = 0; i < packageCount; i++) { string name = inc.ReadString(); - string hash = inc.ReadString(); + UInt32 hashByteCount = inc.ReadVariableUInt32(); + byte[] hashBytes = inc.ReadBytes((int)hashByteCount); UInt64 workshopId = inc.ReadUInt64(); UInt32 installTimeDiffSeconds = inc.ReadUInt32(); DateTime installTime = DateTime.UtcNow + TimeSpan.FromSeconds(installTimeDiffSeconds); - var pkg = new ServerContentPackage(name, hash, workshopId, installTime); - if (pkg.CorePackage != null) - { - corePackage = pkg; - } - else if (pkg.RegularPackage != null) - { - regularPackages.Add(pkg); - } - else - { - missingPackages.Add(pkg); - } - } - - if (missingPackages.Count > 0) - { - var nonDownloadable = missingPackages.Where(p => p.WorkshopId == 0); - var mismatchedButDownloaded = missingPackages.Where(remote => - { - return ContentPackage.AllPackages.Any(local => - local.SteamWorkshopId != 0 && /* is a Workshop item */ - remote.WorkshopId == local.SteamWorkshopId && /* ids match */ - remote.InstallTime < local.InstallTime/* remote is older than local */); - }); - - if (mismatchedButDownloaded.Any()) - { - string disconnectMsg; - if (mismatchedButDownloaded.Count() == 1) - { - disconnectMsg = $"DisconnectMessage.MismatchedWorkshopMod~[incompatiblecontentpackage]={GetPackageStr(mismatchedButDownloaded.First())}"; - } - else - { - List packageStrs = new List(); - mismatchedButDownloaded.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - disconnectMsg = $"DisconnectMessage.MismatchedWorkshopMods~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"; - } - Close(disconnectMsg, disableReconnect: true); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.MissingContentPackage + "/" + disconnectMsg); - } - else if (nonDownloadable.Any()) - { - string disconnectMsg; - if (nonDownloadable.Count() == 1) - { - disconnectMsg = $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(nonDownloadable.First())}"; - } - else - { - List packageStrs = new List(); - nonDownloadable.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - disconnectMsg = $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"; - } - Close(disconnectMsg, disableReconnect: true); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.MissingContentPackage + "/" + disconnectMsg); - } - else - { - Close(disableReconnect: true); - - string missingModNames = "\n"; - int displayedModCount = 0; - foreach (ServerContentPackage missingPackage in missingPackages) - { - missingModNames += "\n- " + GetPackageStr(missingPackage); - displayedModCount++; - if (GUI.Font.MeasureString(missingModNames).Y > GameMain.GraphicsHeight * 0.5f) - { - missingModNames += "\n\n" + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (missingPackages.Count - displayedModCount).ToString()); - break; - } - } - missingModNames += "\n\n"; - - var msgBox = new GUIMessageBox( - TextManager.Get("WorkshopItemDownloadTitle"), - TextManager.GetWithVariable("WorkshopItemDownloadPrompt", "[items]", missingModNames), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => - { - GameMain.ServerListScreen.Select(); - IEnumerable downloads = - missingPackages.Select(p => new ServerListScreen.PendingWorkshopDownload(p.Hash, p.WorkshopId)); - GameMain.ServerListScreen.DownloadWorkshopItems(downloads, serverName, ServerConnection.EndPointString); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - } - - return; + var pkg = new ServerContentPackage(name, Md5Hash.BytesAsHash(hashBytes), workshopId, installTime); + serverPackages.Add(pkg); } if (!contentPackageOrderReceived) { - GameMain.Config.BackUpModOrder(); - GameMain.Config.SwapPackages(corePackage.CorePackage, regularPackages.Select(p => p.RegularPackage).ToList()); - contentPackageOrderReceived = true; + ServerContentPackages = serverPackages.ToImmutableArray(); + SendMsgInternal(DeliveryMethod.Reliable, outMsg); } - - SendMsgInternal(DeliveryMethod.Reliable, outMsg); break; case ConnectionInitialization.Password: if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 0aacf802e..38b49f398 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -36,7 +36,7 @@ namespace Barotrauma.Networking netPeerConfiguration = new NetPeerConfiguration("barotrauma") { - UseDualModeSockets = GameMain.Config.UseDualModeSockets + UseDualModeSockets = GameSettings.CurrentConfig.UseDualModeSockets }; netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt @@ -175,13 +175,13 @@ namespace Barotrauma.Networking isActive = false; - netClient.Shutdown(msg ?? TextManager.Get("Disconnecting")); + netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value); netClient = null; steamAuthTicket?.Cancel(); steamAuthTicket = null; OnDisconnect?.Invoke(disableReconnect); } - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!isActive) { return; } @@ -208,7 +208,7 @@ namespace Barotrauma.Networking NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); lidgrenMsg.Write((UInt16)length); lidgrenMsg.Write(msgData, 0, length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 4100bb358..d6a96d556 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -242,7 +242,7 @@ namespace Barotrauma.Networking incomingDataMessages.Clear(); } - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!isActive) { return; } @@ -250,7 +250,7 @@ namespace Barotrauma.Networking buf[0] = (byte)deliveryMethod; byte[] bufAux = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref bufAux, out bool isCompressed, out int length); + msg.PrepareForSending(ref bufAux, compressPastThreshold, out bool isCompressed, out int length); buf[1] = (byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 9f4bc841b..d96fb7c5f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -429,13 +429,13 @@ namespace Barotrauma.Networking Steamworks.SteamUser.OnValidateAuthTicketResponse -= OnAuthChange; } - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!isActive) { return; } IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); msgToSend.Write(selfSteamID); msgToSend.Write(selfSteamID); msgToSend.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 596f5c8d0..4057e960c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -52,7 +52,7 @@ namespace Barotrauma.Networking if (Character.Controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } var respawnPrompt = new GUIMessageBox( TextManager.Get("tutorial.tryagainheader"), TextManager.Get("respawnquestionprompt"), - new string[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) + new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) { UserData = "respawnquestionprompt" }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index f0abd892a..44e51dfdd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking public bool? FriendlyFireEnabled; public bool? AllowRespawn; public YesNoMaybe? TraitorsEnabled; - public string GameMode; + public Identifier GameMode; public PlayStyle? PlayStyle; public bool Recent; @@ -103,7 +103,7 @@ namespace Barotrauma.Networking frame.ClearChildren(); - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUI.LargeFont) + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont) { ToolTip = ServerName }; @@ -143,7 +143,7 @@ namespace Barotrauma.Networking var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.06f) }, TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, - font: GUI.SmallFont, textAlignment: Alignment.Center, + font: GUIStyle.SmallFont, textAlignment: Alignment.Center, color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); playStyleName.RectTransform.IsFixedSize = true; @@ -188,7 +188,7 @@ namespace Barotrauma.Networking new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true }; - var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUI.SmallFont, wrap: true) + var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false }; @@ -197,7 +197,7 @@ namespace Barotrauma.Networking var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), - TextManager.Get(string.IsNullOrEmpty(GameMode) ? "Unknown" : "GameMode." + GameMode, returnNull: true) ?? GameMode, + TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), textAlignment: Alignment.Right); GUITextBlock playStyleText = null; @@ -218,11 +218,11 @@ namespace Barotrauma.Networking subSelection.TextSize.X + subSelection.GetChild().TextSize.X > subSelection.Rect.Width || modeSelection.TextSize.X + modeSelection.GetChild().TextSize.X > modeSelection.Rect.Width) { - gameMode.Font = subSelection.Font = modeSelection.Font = GUI.SmallFont; - gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUI.SmallFont; + gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont; + gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUIStyle.SmallFont; if (playStyleText != null) { - playStyleText.Font = playStyleText.GetChild().Font = GUI.SmallFont; + playStyleText.Font = playStyleText.GetChild().Font = GUIStyle.SmallFont; } } @@ -268,7 +268,7 @@ namespace Barotrauma.Networking }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUI.SubHeadingFont); + TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) { ScrollBarVisible = true }; if (ContentPackageNames.Count == 0) @@ -289,7 +289,7 @@ namespace Barotrauma.Networking }; if (i < ContentPackageHashes.Count) { - if (ContentPackage.AllPackages.Any(cp => cp.MD5hash.Hash == ContentPackageHashes[i])) + if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == ContentPackageHashes[i])) { packageText.Selected = true; continue; @@ -298,14 +298,14 @@ namespace Barotrauma.Networking //workshop download link found if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) { - packageText.TextColor = GUI.Style.Yellow; + packageText.TextColor = GUIStyle.Yellow; packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); } else //no package or workshop download link found, tough luck { - packageText.TextColor = GUI.Style.Red; + packageText.TextColor = GUIStyle.Red; packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage", - new string[2] { "[contentpackage]", "[hash]" }, new string[2] { ContentPackageNames[i], ContentPackageHashes[i] }); + ("[contentpackage]", ContentPackageNames[i]), ("[hash]", ContentPackageHashes[i])); } } } @@ -361,7 +361,7 @@ namespace Barotrauma.Networking info.RespondedToSteamQuery = null; - info.GameMode = element.GetAttributeString("GameMode", ""); + info.GameMode = element.GetAttributeIdentifier("GameMode", Identifier.Empty); info.GameVersion = element.GetAttributeString("GameVersion", ""); int maxPlayersElement = element.GetAttributeInt("MaxPlayers", 0); @@ -515,7 +515,7 @@ namespace Barotrauma.Networking element.SetAttributeValue("OwnerID", SteamManager.SteamIDUInt64ToString(OwnerID)); } - element.SetAttributeValue("GameMode", GameMode ?? ""); + element.SetAttributeValue("GameMode", GameMode); element.SetAttributeValue("GameVersion", GameVersion ?? ""); element.SetAttributeValue("MaxPlayers", MaxPlayers); if (PlayStyle.HasValue) { element.SetAttributeValue("PlayStyle", PlayStyle.Value.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 8e18c1c82..19a9ba612 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -49,7 +49,7 @@ namespace Barotrauma.Networking List tickBoxes = new List(); foreach (MessageType msgType in Enum.GetValues(typeof(MessageType))) { - var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, 30), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUI.SmallFont) + var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, 30), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUIStyle.SmallFont) { Selected = true, TextColor = messageColor[msgType], @@ -84,8 +84,8 @@ namespace Barotrauma.Networking isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), filterArea.RectTransform), TextManager.Get("ServerLog.Filter"), - font: GUI.SubHeadingFont); - GUITextBox searchBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.SmallFont, createClearButton: true); + font: GUIStyle.SubHeadingFont); + GUITextBox searchBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.SmallFont, createClearButton: true); searchBox.OnTextChanged += (textBox, text) => { msgFilter = text; @@ -146,7 +146,7 @@ namespace Barotrauma.Networking List tickBoxes = new List(); foreach (MessageType msgType in Enum.GetValues(typeof(MessageType))) { - var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUI.SmallFont) + var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUIStyle.SmallFont) { Selected = true, TextColor = messageColor[msgType], @@ -191,9 +191,10 @@ namespace Barotrauma.Networking Anchor anchor = Anchor.TopLeft; Pivot pivot = Pivot.TopLeft; - if (line.RichData != null) + RichString richString = line.Text as RichString; + if (richString != null && richString.RichTextData.HasValue) { - foreach (var data in line.RichData) + foreach (var data in richString.RichTextData.Value) { if (!UInt64.TryParse(data.Metadata, out ulong id)) { return; } Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) @@ -215,7 +216,7 @@ namespace Barotrauma.Networking } var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), (textContainer ?? listBox.Content).RectTransform, anchor, pivot), - line.RichData, line.SanitizedText, wrap: true, font: GUI.SmallFont) + line.Text, wrap: true, font: GUIStyle.SmallFont) { TextColor = messageColor[line.Type], Visible = !msgTypeHidden[(int)line.Type], @@ -235,9 +236,9 @@ namespace Barotrauma.Networking textBlock.RectTransform.SetAsFirstChild(); } - if (line.RichData != null) + if (richString != null && richString.RichTextData.HasValue) { - foreach (var data in line.RichData) + foreach (var data in richString.RichTextData.Value) { textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 783d52326..430df186b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -77,18 +77,18 @@ namespace Barotrauma.Networking } } } - private Dictionary tempMonsterEnabled; + private Dictionary tempMonsterEnabled; partial void InitProjSpecific() { var properties = TypeDescriptor.GetProperties(GetType()).Cast(); - SerializableProperties = new Dictionary(); + SerializableProperties = new Dictionary(); foreach (var property in properties) { SerializableProperty objProperty = new SerializableProperty(property); - SerializableProperties.Add(property.Name.ToLowerInvariant(), objProperty); + SerializableProperties.Add(property.Name.ToIdentifier(), objProperty); } } @@ -168,7 +168,16 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) + public void ClientAdminWrite( + NetFlags dataToSend, + int? missionTypeOr = null, + int? missionTypeAnd = null, + float? levelDifficulty = null, + bool? autoRestart = null, + int traitorSetting = 0, + int botCount = 0, + int botSpawnMode = 0, + bool? useRespawnShuttle = null) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -208,7 +217,7 @@ namespace Barotrauma.Networking outMsg.Write(count); foreach (KeyValuePair prop in changedProperties) { - DebugConsole.NewMessage(prop.Value.Name, Color.Lime); + DebugConsole.NewMessage(prop.Value.Name.Value, Color.Lime); outMsg.Write(prop.Key); prop.Value.Write(outMsg, prop.Value.GUIComponentValue); } @@ -315,7 +324,7 @@ namespace Barotrauma.Networking RelativeSpacing = 0.02f }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("Settings"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("Settings"), font: GUIStyle.LargeFont); var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedFrame.RectTransform), isHorizontal: true) { @@ -326,12 +335,9 @@ namespace Barotrauma.Networking var tabContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform), style: "InnerFrame"); //tabs - var tabValues = Enum.GetValues(typeof(SettingsTab)).Cast().ToArray(); - string[] tabNames = new string[tabValues.Count()]; - for (int i = 0; i < tabNames.Length; i++) - { - tabNames[i] = TextManager.Get("ServerSettings" + tabValues[i] + "Tab"); - } + LocalizedString[] tabNames = + Enum.GetValues(typeof(SettingsTab)).Cast() + .Select(tv => TextManager.Get("ServerSettings" + tv + "Tab")).ToArray(); settingsTabs = new GUIFrame[tabNames.Length]; tabButtons = new GUIButton[tabNames.Length]; for (int i = 0; i < tabNames.Length; i++) @@ -367,7 +373,7 @@ namespace Barotrauma.Networking //*********************************************** // Sub Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUIStyle.SubHeadingFont); var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) { Stretch = true, @@ -377,7 +383,7 @@ namespace Barotrauma.Networking GUIRadioButtonGroup selectionMode = new GUIRadioButtonGroup(); for (int i = 0; i < 3; i++) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont, style: "GUIRadioButton"); + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); selectionMode.AddRadioButton(i, selectionTick); } selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); @@ -386,7 +392,7 @@ namespace Barotrauma.Networking GetPropertyData(nameof(SubSelectionMode)).AssignGUIComponent(selectionMode); // Mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection"), font: GUIStyle.SubHeadingFont); selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) { Stretch = true, @@ -396,7 +402,7 @@ namespace Barotrauma.Networking selectionMode = new GUIRadioButtonGroup(); for (int i = 0; i < 3; i++) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont, style: "GUIRadioButton"); + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); selectionMode.AddRadioButton(i, selectionTick); } selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); @@ -413,7 +419,7 @@ namespace Barotrauma.Networking //*********************************************** - string autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; + LocalizedString autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; var startIntervalText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), autoRestartDelayLabel); var startIntervalSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), barSize: 0.1f, style: "GUISlider") { @@ -437,7 +443,7 @@ namespace Barotrauma.Networking GetPropertyData(nameof(StartWhenClientsReady)).AssignGUIComponent(startWhenClientsReady); CreateLabeledSlider(serverTab, "ServerSettingsStartWhenClientsReadyRatio", out GUIScrollBar slider, out GUITextBlock sliderLabel); - string clientsReadyRequiredLabel = sliderLabel.Text; + LocalizedString clientsReadyRequiredLabel = sliderLabel.Text; slider.Step = 0.2f; slider.Range = new Vector2(0.5f, 1.0f); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -481,7 +487,7 @@ namespace Barotrauma.Networking }; // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.16f), roundsTab.RectTransform)) { AutoHideScrollBar = true, @@ -493,7 +499,7 @@ namespace Barotrauma.Networking GUIRadioButtonGroup selectionPlayStyle = new GUIRadioButtonGroup(); foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.32f, 0.49f), playstyleList.Content.RectTransform), TextManager.Get("servertag." + playStyle), font: GUI.SmallFont, style: "GUIRadioButton") + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.32f, 0.49f), playstyleList.Content.RectTransform), TextManager.Get("servertag." + playStyle), font: GUIStyle.SmallFont, style: "GUIRadioButton") { ToolTip = TextManager.Get("servertagdescription." + playStyle) }; @@ -510,7 +516,7 @@ namespace Barotrauma.Networking CreateLabeledSlider(roundsTab, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); - string endRoundLabel = sliderLabel.Text; + LocalizedString endRoundLabel = sliderLabel.Text; slider.Step = 0.2f; slider.Range = new Vector2(0.5f, 1.0f); GetPropertyData(nameof(EndVoteRequiredRatio)).AssignGUIComponent(slider); @@ -526,7 +532,7 @@ namespace Barotrauma.Networking GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); CreateLabeledSlider(roundsTab, "ServerSettingsRespawnInterval", out slider, out sliderLabel); - string intervalLabel = sliderLabel.Text; + LocalizedString intervalLabel = sliderLabel.Text; slider.Range = new Vector2(10.0f, 600.0f); slider.StepValue = 10.0f; GetPropertyData(nameof(RespawnInterval)).AssignGUIComponent(slider); @@ -549,11 +555,11 @@ namespace Barotrauma.Networking ToolTip = TextManager.Get("ServerSettingsMinRespawnToolTip") }; - string minRespawnLabel = TextManager.Get("ServerSettingsMinRespawn") + " "; + LocalizedString minRespawnLabel = TextManager.Get("ServerSettingsMinRespawn") + " "; CreateLabeledSlider(minRespawnLayout, "", out slider, out sliderLabel); sliderLabel.RectTransform.RelativeSize = Vector2.Zero; slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = minRespawnText.RawToolTip; + slider.ToolTip = minRespawnText.ToolTip; slider.UserData = minRespawnText; slider.Step = 0.1f; slider.Range = new Vector2(0.0f, 1.0f); @@ -573,11 +579,11 @@ namespace Barotrauma.Networking ToolTip = TextManager.Get("ServerSettingsRespawnDurationToolTip") }; - string respawnDurationLabel = TextManager.Get("ServerSettingsRespawnDuration") + " "; + LocalizedString respawnDurationLabel = TextManager.Get("ServerSettingsRespawnDuration") + " "; CreateLabeledSlider(respawnDurationLayout, "", out slider, out sliderLabel); sliderLabel.RectTransform.RelativeSize = Vector2.Zero; slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = respawnDurationText.RawToolTip; + slider.ToolTip = respawnDurationText.ToolTip; slider.UserData = respawnDurationText; slider.Step = 0.1f; slider.Range = new Vector2(60.0f, 660.0f); @@ -620,7 +626,7 @@ namespace Barotrauma.Networking LosMode[] losModes = (LosMode[])Enum.GetValues(typeof(LosMode)); for (int i = 0; i < losModes.Length; i++) { - var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), losModeRadioButtonLayout.RectTransform), TextManager.Get($"LosMode{losModes[i]}"), font: GUI.SmallFont, style: "GUIRadioButton"); + var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), losModeRadioButtonLayout.RectTransform), TextManager.Get($"LosMode{losModes[i]}"), font: GUIStyle.SmallFont, style: "GUIRadioButton"); losModeRadioButtonGroup.AddRadioButton(i, losTick); } GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); @@ -663,13 +669,13 @@ namespace Barotrauma.Networking }; InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys.ToList(); - tempMonsterEnabled = new Dictionary(MonsterEnabled); - foreach (string s in monsterNames) + List monsterNames = MonsterEnabled.Keys.ToList(); + tempMonsterEnabled = new Dictionary(MonsterEnabled); + foreach (Identifier s in monsterNames) { - string translatedLabel = TextManager.Get($"Character.{s}", true); + LocalizedString translatedLabel = TextManager.Get($"Character.{s}").Fallback(s.Value); var monsterEnabledBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), monsterFrame.Content.RectTransform) { MinSize = new Point(0, 25) }, - label: translatedLabel ?? s) + label: translatedLabel) { Selected = tempMonsterEnabled[s], OnSelected = (GUITickBox tb) => @@ -722,10 +728,10 @@ namespace Barotrauma.Networking RelativeSpacing = 0.05f }; - if (ip.InventoryIcon != null || ip.sprite != null) + if (ip.InventoryIcon != null || ip.Sprite != null) { GUIImage img = new GUIImage(new RectTransform(new Point(itemFrame.Rect.Height), itemFrame.RectTransform), - ip.InventoryIcon ?? ip.sprite, scaleToFit: true) + ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) { CanBeFocused = false }; @@ -733,7 +739,7 @@ namespace Barotrauma.Networking } new GUITextBlock(new RectTransform(new Vector2(0.75f, 1.0f), itemFrame.RectTransform), - ip.Name, font: GUI.SmallFont) + ip.Name, font: GUIStyle.SmallFont) { Wrap = true, CanBeFocused = false @@ -826,7 +832,7 @@ namespace Barotrauma.Networking tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); CreateLabeledSlider(antigriefingTab, "ServerSettingsKickVotesRequired", out slider, out sliderLabel); - string votesRequiredLabel = sliderLabel.Text + " "; + LocalizedString votesRequiredLabel = sliderLabel.Text + " "; slider.Step = 0.2f; slider.Range = new Vector2(0.5f, 1.0f); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -838,7 +844,7 @@ namespace Barotrauma.Networking slider.OnMoved(slider, slider.BarScroll); CreateLabeledSlider(antigriefingTab, "ServerSettingsAutobanTime", out slider, out sliderLabel); - string autobanLabel = sliderLabel.Text + " "; + LocalizedString autobanLabel = sliderLabel.Text + " "; slider.Step = 0.01f; slider.Range = new Vector2(0.0f, MaxAutoBanTime); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -946,7 +952,7 @@ namespace Barotrauma.Networking slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider"); label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), - string.IsNullOrEmpty(labelTag) ? "" : TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + string.IsNullOrEmpty(labelTag) ? "" : TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y); container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y); @@ -965,7 +971,7 @@ namespace Barotrauma.Networking }; var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont) + TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) { AutoScaleHorizontal = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs deleted file mode 100644 index a72a282f0..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ /dev/null @@ -1,1740 +0,0 @@ -using Barotrauma.Extensions; -using Barotrauma.IO; -using Barotrauma.Networking; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Linq; -using Color = Microsoft.Xna.Framework.Color; - -namespace Barotrauma.Steam -{ - static partial class SteamManager - { - private static readonly Dictionary modCopiesInProgress = new Dictionary(); - - private static void InitializeProjectSpecific() - { - if (isInitialized) { return; } - - try - { - Steamworks.SteamClient.Init(AppID, false); - isInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid; - - if (isInitialized) - { - DebugConsole.NewMessage("Logged in as " + GetUsername() + " (SteamID " + SteamIDUInt64ToString(GetSteamID()) + ")"); - - popularTags.Clear(); - int i = 0; - foreach (KeyValuePair commonness in tagCommonness) - { - popularTags.Insert(i, commonness.Key); - i++; - } - } - - Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking; - } - catch (DllNotFoundException) - { - isInitialized = false; - initializationErrors.Add("SteamDllNotFound"); - } - catch (Exception e) - { -#if !DEBUG - DebugConsole.ThrowError("SteamManager initialization threw an exception", e); -#endif - isInitialized = false; - initializationErrors.Add("SteamClientInitFailed"); - } - - if (!isInitialized) - { - try - { - if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } - } - catch (Exception e) - { - if (GameSettings.VerboseLogging) DebugConsole.ThrowError("Disposing Steam client failed.", e); - } - } - } - - public static bool NetworkingDebugLog = false; - - private static void LogSteamworksNetworking(Steamworks.NetDebugOutput nType, string pszMsg) - { - DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange); - } - - public static void SetSteamworksNetworkingDebugLog(bool enabled) - { - if (enabled) - { - Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.Everything; - } - else - { - Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.None; - } - } - - public static async Task InitRelayNetworkAccess() - { - if (!IsInitialized) { return; } - - await Task.Yield(); - Steamworks.SteamNetworkingUtils.InitRelayNetworkAccess(); - - SetSteamworksNetworkingDebugLog(true); - var status = Steamworks.SteamNetworkingUtils.Status; - while (status.Avail != Steamworks.SteamNetworkingAvailability.Current) - { - if (status.Avail == Steamworks.SteamNetworkingAvailability.CannotTry || - status.Avail == Steamworks.SteamNetworkingAvailability.Previously || - status.Avail == Steamworks.SteamNetworkingAvailability.Failed) - { - DebugConsole.ThrowError($"Failed to initialize Steamworks network relay: " + - $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + - $"{Steamworks.SteamNetworkingUtils.Status.AvailNetConfig}, " + - $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + - $"{Steamworks.SteamNetworkingUtils.Status.Msg}"); - break; - } - await Task.Delay(25); - status = Steamworks.SteamNetworkingUtils.Status; - } - SetSteamworksNetworkingDebugLog(false); - } - - private enum LobbyState - { - NotConnected, - Creating, - Owner, - Joining, - Joined - } - private static UInt64 lobbyID = 0; - private static LobbyState lobbyState = LobbyState.NotConnected; - private static Steamworks.Data.Lobby? currentLobby; - public static UInt64 CurrentLobbyID - { - get { return currentLobby?.Id ?? 0; } - } - - public static void CreateLobby(ServerSettings serverSettings) - { - if (lobbyState != LobbyState.NotConnected) { return; } - lobbyState = LobbyState.Creating; - TaskPool.Add("CreateLobbyAsync", Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10), - (lobby) => - { - if (lobbyState != LobbyState.Creating) - { - LeaveLobby(); - return; - } - - lobby.TryGetResult(out currentLobby); - if (currentLobby == null) - { - DebugConsole.ThrowError("Failed to create Steam lobby: returned lobby was null"); - lobbyState = LobbyState.NotConnected; - return; - } - - if (currentLobby.Value.Result != Steamworks.Result.OK) - { - DebugConsole.ThrowError($"Failed to create Steam lobby: result was {currentLobby.Value.Result}"); - lobbyState = LobbyState.NotConnected; - return; - } - - DebugConsole.NewMessage("Lobby created!", Microsoft.Xna.Framework.Color.Lime); - - lobbyState = LobbyState.Owner; - lobbyID = (currentLobby?.Id).Value; - - if (serverSettings.IsPublic) - { - currentLobby?.SetPublic(); - } - else - { - currentLobby?.SetFriendsOnly(); - } - currentLobby?.SetJoinable(true); - - UpdateLobby(serverSettings); - }); - } - - public static void UpdateLobby(ServerSettings serverSettings) - { - if (GameMain.Client == null) - { - LeaveLobby(); - } - - if (lobbyState == LobbyState.NotConnected) - { - CreateLobby(serverSettings); - } - - if (lobbyState != LobbyState.Owner) - { - return; - } - - var contentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); - - currentLobby?.SetData("name", serverSettings.ServerName); - currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); - currentLobby?.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); - //currentLobby?.SetData("hostipaddress", lobbyIP); - string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); - currentLobby?.SetData("pinglocation", pingLocation ?? ""); - currentLobby?.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID())); - currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString()); - - currentLobby?.SetData("message", serverSettings.ServerMessageText); - currentLobby?.SetData("version", GameMain.Version.ToString()); - - currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); - currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash))); - currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); - currentLobby?.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString()); - currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); - currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); - currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); - currentLobby?.SetData("allowspectating", serverSettings.AllowSpectating.ToString()); - currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); - currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); - currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); - currentLobby?.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); - currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); - currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); - currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier ?? ""); - - DebugConsole.Log("Lobby updated!"); - } - - public static void LeaveLobby() - { - if (lobbyState != LobbyState.NotConnected) - { - currentLobby?.Leave(); currentLobby = null; - lobbyState = LobbyState.NotConnected; - - lobbyID = 0; - - Steamworks.SteamMatchmaking.ResetActions(); - } - } - public static void JoinLobby(UInt64 id, bool joinServer) - { - if (currentLobby.HasValue && currentLobby.Value.Id == id) { return; } - if (lobbyID == id) { return; } - lobbyState = LobbyState.Joining; - lobbyID = id; - - TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), - (lobby) => - { - lobby.TryGetResult(out currentLobby); - lobbyState = LobbyState.Joined; - lobbyID = (currentLobby?.Id).Value; - if (joinServer) - { - GameMain.Instance.ConnectLobby = 0; - GameMain.Instance.ConnectName = currentLobby?.GetData("servername"); - GameMain.Instance.ConnectEndpoint = SteamIDUInt64ToString((currentLobby?.Owner.Id).Value); - } - }); - } - - public static bool GetServers(Action addToServerList, Action serverQueryFinished) - { - if (!isInitialized) { return false; } - - int doneTasks = 0; - void taskDone() - { - doneTasks++; - if (doneTasks >= 2) - { - serverQueryFinished?.Invoke(); - serverQueryFinished = null; - } - } - - - Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => - { - DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); - }; - - TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(), - (t) => - { - Steamworks.Dispatch.OnDebugCallback = null; - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies"); - taskDone(); - return; - } - t.TryGetResult(out List lobbies); - IEnumerable lobbyAddCoroutine() - { - int i = 0; - foreach (var lobby in lobbies ?? Enumerable.Empty()) - { - if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } - - ServerInfo serverInfo = new ServerInfo(); - serverInfo.ServerName = lobby.GetData("name"); - serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); - serverInfo.LobbyID = lobby.Id; - bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword); - serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0; - serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1; - serverInfo.RespondedToSteamQuery = true; - - AssignLobbyDataToServerInfo(lobby, serverInfo); - - addToServerList(serverInfo); - i++; - if (i >= 16) { yield return CoroutineStatus.Running; i = 0; } - } - taskDone(); - yield return CoroutineStatus.Success; - } - CoroutineManager.StartCoroutine(lobbyAddCoroutine()); - }); - - Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); - void onServer(Steamworks.Data.ServerInfo info, bool responsive) - { - if (string.IsNullOrEmpty(info.Name)) { return; } - - ServerInfo serverInfo = new ServerInfo - { - ServerName = info.Name, - HasPassword = info.Passworded, - IP = info.Address.ToString(), - Port = info.ConnectionPort.ToString(), - PlayerCount = info.Players, - MaxPlayers = info.MaxPlayers, - RespondedToSteamQuery = responsive - }; - - if (responsive) - { - TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(), - (t) => - { - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name); - return; - } - - t.TryGetResult(out Dictionary rules); - AssignServerRulesToServerInfo(rules, serverInfo); - - addToServerList(serverInfo); - }); - } - else - { - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); - } - - } - serverQuery.OnResponsiveServer += (info) => onServer(info, true); - serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); - - TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(), - (t) => - { - serverQuery.Dispose(); - taskDone(); - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); - return; - } - }); - - return true; - } - - public static async Task> LobbyQueryRequest() - { - List allLobbies = new List(); - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() - .FilterDistanceWorldwide() - .WithMaxResults(50); - //steamworks seems to unable to retrieve more than 50 - //lobbies per request; to work around this, we'll make - //up to 10 requests, asking to ignore all previous results - //in each subsequent request - for (int i = 0; i < 10; i++) - { - Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); - if (lobbies == null) { break; } - foreach (var l in lobbies) - { - lobbyQuery = lobbyQuery - .WithoutKeyValue("lobbyowner", l.GetData("lobbyowner")); - } - allLobbies.AddRange(lobbies); - } - - //make sure all returned lobbies are distinct, don't want any duplicates here - return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList(); - } - - public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - serverInfo.ServerMessage = lobby.GetData("message"); - serverInfo.GameVersion = lobby.GetData("version"); - - serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); - serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); - - string workshopIdData = lobby.GetData("contentpackageid"); - if (!string.IsNullOrEmpty(workshopIdData)) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData)); - } - else - { - string[] workshopUrls = lobby.GetData("contentpackageurl").Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist"); - if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } - if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } - - serverInfo.AllowSpectating = getLobbyBool("allowspectating"); - serverInfo.AllowRespawn = getLobbyBool("allowrespawn"); - serverInfo.VoipEnabled = getLobbyBool("voicechatenabled"); - serverInfo.KarmaEnabled = getLobbyBool("karmaenabled"); - serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled"); - if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } - - serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; - serverInfo.GameMode = lobby.GetData("gamemode") ?? ""; - if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - - string pingLocation = lobby.GetData("pinglocation"); - if (!string.IsNullOrEmpty(pingLocation)) - { - serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation); - } - - bool? getLobbyBool(string key) - { - string data = lobby.GetData(key); - if (string.IsNullOrEmpty(data)) { return null; } - return data == "True" || data == "true"; - } - } - - public static void AssignServerRulesToServerInfo(Dictionary rules, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - if (rules == null) { return; } - - if (rules.ContainsKey("message")) serverInfo.ServerMessage = rules["message"]; - if (rules.ContainsKey("version")) serverInfo.GameVersion = rules["version"]; - - if (rules.ContainsKey("playercount")) - { - if (int.TryParse(rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; - } - - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - if (rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); - if (rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); - if (rules.ContainsKey("contentpackageid")) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"])); - } - else if (rules.ContainsKey("contentpackageurl")) - { - string[] workshopUrls = rules["contentpackageurl"].Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - if (rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = rules["usingwhitelist"] == "True"; - if (rules.ContainsKey("modeselectionmode")) - { - if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode; - } - if (rules.ContainsKey("subselectionmode")) - { - if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode; - } - if (rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = rules["allowspectating"] == "True"; - if (rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = rules["allowrespawn"] == "True"; - if (rules.ContainsKey("voicechatenabled")) serverInfo.VoipEnabled = rules["voicechatenabled"] == "True"; - if (rules.ContainsKey("traitors")) - { - if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; - } - - if (rules.ContainsKey("gamestarted")) serverInfo.GameStarted = rules["gamestarted"] == "True"; - if (rules.ContainsKey("gamemode")) - { - serverInfo.GameMode = rules["gamemode"]; - } - if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle)) - { - serverInfo.PlayStyle = playStyle; - } - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - } - -#region Connecting to servers - - public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; - - DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); - Steamworks.BeginAuthResult startResult = Steamworks.SteamUser.BeginAuthSession(authTicketData, clientSteamID); - if (startResult != Steamworks.BeginAuthResult.OK) - { - DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); - } - - return startResult; - } - - public static void StopAuthSession(ulong clientSteamID) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) return; - - DebugConsole.NewMessage("SteamManager ending auth session with Steam client " + clientSteamID); - Steamworks.SteamUser.EndAuthSession(clientSteamID); - } - -#endregion - -#region Workshop - - public const string WorkshopItemPreviewImageFolder = "Workshop"; - public const string PreviewImageName = "PreviewImage.png"; - public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; - - private static Sprite defaultPreviewImage; - public static Sprite DefaultPreviewImage - { - get - { - if (defaultPreviewImage == null) - { - defaultPreviewImage = new Sprite(DefaultPreviewImagePath, sourceRectangle: null); - } - return defaultPreviewImage; - } - } - - private static async Task> GetWorkshopItemsAsync(Steamworks.Ugc.Query query, int clampResults = 0, Predicate itemPredicate=null) - { - await Task.Yield(); - - int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); - - List retVal = new List(); - while (resultPage.HasValue && resultPage?.ResultCount > 0) - { - if (itemPredicate != null) - { - retVal.AddRange(resultPage.Value.Entries.Where(it => itemPredicate(it))); - } - else - { - retVal.AddRange(resultPage.Value.Entries); - } - - if (clampResults > 0 && retVal.Count >= clampResults) - { - retVal = retVal.Take(clampResults).ToList(); - break; - } - - pageIndex++; - resultPage = await query.GetPageAsync(pageIndex); - } - - return retVal; - } - - public static void GetSubscribedWorkshopItems(Action> onItemsFound, List requireTags = null) - { - if (!isInitialized) return; - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .RankedByTotalUniqueSubscriptions() - .WhereUserSubscribed() - .WithLongDescription(); - if (requireTags != null) { query = query.WithTags(requireTags); } - - TaskPool.Add("GetSubscribedWorkshopItems", GetWorkshopItemsAsync(query), (task) => - { - task.TryGetResult(out List result); onItemsFound?.Invoke(result); - }); - } - - public static void GetPopularWorkshopItems(Action> onItemsFound, int amount, List requireTags = null) - { - if (!isInitialized) return; - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .RankedByTrend() - .WithLongDescription(); - if (requireTags != null) query.WithTags(requireTags); - - TaskPool.Add("GetPopularWorkshopItems", GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => - { - task.TryGetResult(out List entries); - - //count the number of each unique tag - foreach (var item in entries) - { - foreach (string tag in item.Tags) - { - if (string.IsNullOrEmpty(tag)) { continue; } - string caseInvariantTag = tag.ToLowerInvariant(); - if (!tagCommonness.ContainsKey(caseInvariantTag)) - { - tagCommonness[caseInvariantTag] = 1; - } - else - { - tagCommonness[caseInvariantTag]++; - } - } - } - //populate the popularTags list with tags sorted by commonness - popularTags.Clear(); - foreach (KeyValuePair tagCommonnessKVP in tagCommonness) - { - int i = 0; - while (i < popularTags.Count && - tagCommonness[popularTags[i]] > tagCommonnessKVP.Value) - { - i++; - } - popularTags.Insert(i, tagCommonnessKVP.Key); - } - onItemsFound?.Invoke(entries); - }); - } - - public static void GetPublishedWorkshopItems(Action> onItemsFound, List requireTags = null) - { - if (!isInitialized) return; - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .RankedByPublicationDate() - .WhereUserPublished() - .WithLongDescription(); - if (requireTags != null) query.WithTags(requireTags); - - TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => - { - task.TryGetResult(out List result); onItemsFound?.Invoke(result); - }); - } - - private static readonly HashSet pendingWorkshopSubscriptions = new HashSet(); - - public static void SubscribeToWorkshopItem(ulong id, Action onInstalled = null) - { - if (!isInitialized) return; - - if (id == 0) { return; } - - if (pendingWorkshopSubscriptions.Contains(id)) { return; } - - pendingWorkshopSubscriptions.Add(id); - TaskPool.Add( - $"SubscribeToWorkshopItem({id})", - Task.Run(async () => - { - Steamworks.Ugc.Item? item = await Steamworks.SteamUGC.QueryFileAsync(id); - - if (!item.HasValue) - { - DebugConsole.ThrowError($"Failed to find a Steam Workshop item with the ID {id}."); - return null; - } - - if (!(item?.IsSubscribed ?? false)) - { - bool subscribed = await item?.Subscribe(); - if (!subscribed) - { - DebugConsole.ThrowError($"Failed to subscribe to Steam Workshop item with the ID {id}."); - return null; - } - } - - return item; - }), - (t) => - { - bool shouldCleanup = true; - if (t.IsFaulted) - { - TaskPool.PrintTaskExceptions(t, $"Workshop subscription task {id} faulted"); - } - else - { - t.TryGetResult(out Steamworks.Ugc.Item? item); - if (item != null) - { - if (item?.IsInstalled ?? false) - { - onInstalled?.Invoke(); - } - else - { - void _onInstalled() - { - onInstalled?.Invoke(); - pendingWorkshopSubscriptions.Remove(id); - } - bool downloading = item?.Download(_onInstalled) ?? false; - if (!downloading) - { - DebugConsole.ThrowError($"Failed to start downloading Steam Workshop item with the ID {id}."); - } - else - { - shouldCleanup = false; - } - } - } - - if (shouldCleanup) - { - pendingWorkshopSubscriptions.Remove(id); - } - } - }); - } - - public static void CreateWorkshopItemStaging(ContentPackage contentPackage, out Steamworks.Ugc.Editor? itemEditor) - { - string folderPath = Path.GetDirectoryName(contentPackage.Path); - if (!Directory.Exists(folderPath)) { Directory.CreateDirectory(folderPath); } - itemEditor = Steamworks.Ugc.Editor.NewCommunityFile - .WithPublicVisibility() - .ForAppId(AppID) - .WithContent(folderPath); - - string previewImagePath = Path.GetFullPath(Path.Combine(folderPath, PreviewImageName)); - - if (!File.Exists(previewImagePath)) - { - File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath); - } - } - - /// - /// Creates a new empty content package - /// - public static void CreateWorkshopItemStaging(string itemName, out Steamworks.Ugc.Editor? itemEditor, out ContentPackage contentPackage) - { - string dirPath = Path.Combine("Mods", ToolBox.RemoveInvalidFileNameChars(itemName)); - Directory.CreateDirectory("Mods"); - Directory.CreateDirectory(dirPath); - - itemEditor = Steamworks.Ugc.Editor.NewCommunityFile -#if DEBUG - .WithPrivateVisibility() -#else - .WithPublicVisibility() -#endif - .ForAppId(AppID) - .WithContent(dirPath); - - string previewImagePath = Path.GetFullPath(Path.Combine(dirPath, PreviewImageName)); - if (!File.Exists(previewImagePath)) - { - File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath); - } - - //create a new content package and include the copied files in it - contentPackage = ContentPackage.CreatePackage(itemName, Path.Combine(dirPath, MetadataFileName), false); - contentPackage.Save(Path.Combine(dirPath, MetadataFileName)); - } - - /// - /// Creates a copy of the specified workshop item in the staging folder and an editor that can be used to edit and update the item - /// - public static bool CreateWorkshopItemStaging(Steamworks.Ugc.Item? existingItem, out Steamworks.Ugc.Editor? itemEditor, out ContentPackage contentPackage) - { - if (!(existingItem?.IsInstalled ?? false)) - { - itemEditor = null; - contentPackage = null; - DebugConsole.ThrowError("Cannot edit the workshop item \"" + (existingItem?.Title ?? "[NULL]") + "\" because it has not been installed."); - return false; - } - - itemEditor = new Steamworks.Ugc.Editor(existingItem.Value.Id) - .ForAppId(AppID) - .WithTitle(existingItem.Value.Title) - .WithTags(existingItem.Value.Tags) - .WithDescription(existingItem.Value.Description); - - if (existingItem.Value.IsPublic) - { - itemEditor = itemEditor?.WithPublicVisibility(); - } - else if (existingItem.Value.IsFriendsOnly) - { - itemEditor = itemEditor?.WithFriendsOnlyVisibility(); - } - else if (existingItem.Value.IsPrivate) - { - itemEditor = itemEditor?.WithPrivateVisibility(); - } - - if (!CheckWorkshopItemInstalled(existingItem)) - { - if (!InstallWorkshopItem(existingItem, out string errorMsg)) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { existingItem?.Title, errorMsg })); - itemEditor = null; - contentPackage = null; - return false; - } - } - - ContentPackage tempContentPackage = new ContentPackage(Path.Combine(existingItem?.Directory, MetadataFileName)) { SteamWorkshopId = existingItem.Value.Id }; - string installedContentPackagePath = Path.GetFullPath(GetWorkshopItemContentPackagePath(tempContentPackage)); - contentPackage = ContentPackage.AllPackages.FirstOrDefault(cp => Path.GetFullPath(cp.Path) == installedContentPackagePath); - - itemEditor = itemEditor?.WithContent(Path.GetDirectoryName(installedContentPackagePath)); - - string previewImagePath = Path.GetFullPath(Path.Combine(itemEditor?.ContentFolder.FullName, PreviewImageName)); - itemEditor = itemEditor?.WithPreviewFile(previewImagePath); - - try - { - if (File.Exists(previewImagePath)) { File.Delete(previewImagePath); } - - Uri baseAddress = new Uri(existingItem?.PreviewImageUrl); - Uri directory = new Uri(baseAddress, "."); // "." == current dir, like MS-DOS - string fileName = Path.GetFileName(baseAddress.LocalPath); - - IRestClient client = new RestClient(directory); - var request = new RestRequest(fileName, Method.GET); - var response = client.Execute(request); - - if (response.ResponseStatus == ResponseStatus.Completed) - { - File.WriteAllBytes(previewImagePath, response.RawBytes); - } - } - - catch (Exception e) - { - string errorMsg = "Failed to save workshop item preview image when creating workshop item staging folder."; - GameAnalyticsManager.AddErrorEventOnce("SteamManager.CreateWorkshopItemStaging:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.Message); - } - - return true; - } - - public class WorkshopPublishStatus - { - public CoroutineHandle Coroutine; - public ContentPackage ContentPackage; - public Steamworks.Ugc.Editor? Item; - public bool? Success; - public Steamworks.Ugc.PublishResult? Result; - public TaskStatus? TaskStatus; - } - - public static WorkshopPublishStatus StartPublishItem(ContentPackage contentPackage, Steamworks.Ugc.Editor? item) - { - if (!isInitialized) return null; - - if (string.IsNullOrEmpty(item?.Title)) - { - DebugConsole.ThrowError("Cannot publish workshop item - title not set."); - return null; - } - if (string.IsNullOrEmpty(item?.ContentFolder?.FullName)) - { - DebugConsole.ThrowError("Cannot publish workshop item \"" + item?.Title + "\" - folder not set."); - return null; - } - if (!contentPackage.Files.Any()) - { - DebugConsole.ThrowError("Cannot publish workshop item \"" + item?.Title + "\" - no files defined."); - return null; - } - - contentPackage.GameVersion = GameMain.Version; - contentPackage.Save(contentPackage.Path); - - if (File.Exists(PreviewImageName)) { File.Delete(PreviewImageName); } - //move the preview image out of the staging folder, it does not need to be included in the folder sent to Workshop - File.Move(Path.GetFullPath(Path.Combine(item?.ContentFolder?.FullName, PreviewImageName)), PreviewImageName); - item = item?.WithPreviewFile(Path.GetFullPath(PreviewImageName)); - - var workshopPublishStatus = new WorkshopPublishStatus() { Item = item, Result = null, Success = null, ContentPackage = contentPackage }; - workshopPublishStatus.Coroutine = CoroutineManager.StartCoroutine(PublishItem(workshopPublishStatus)); - return workshopPublishStatus; - } - - private static IEnumerable PublishItem(WorkshopPublishStatus workshopPublishStatus) - { - if (!isInitialized) - { - yield return CoroutineStatus.Success; - } - - var item = workshopPublishStatus.Item; - var contentPackage = workshopPublishStatus.ContentPackage; - - Task task = item?.SubmitAsync(); - while (!task.IsCompleted) - { - yield return new WaitForSeconds(1.0f); - } - - if (task.Status != TaskStatus.RanToCompletion) - { - workshopPublishStatus.Success = false; - workshopPublishStatus.TaskStatus = task.Status; - - DebugConsole.NewMessage("Publishing workshop item " + item?.Title + " failed: task failed with status " + task.Status.ToString(), Color.Red); - } - else if (!task.Result.Success) - { - workshopPublishStatus.Success = false; - workshopPublishStatus.Result = task.Result; - DebugConsole.NewMessage("Publishing workshop item " + item?.Title + " failed: Workshop result "+task.Result.Result.ToString(), Color.Red); - } - else - { - //nuke the existing steamworks cache for the item we just published - ForceRedownload(task.Result.FileId); - - workshopPublishStatus.Success = true; - workshopPublishStatus.Result = task.Result; - DebugConsole.NewMessage("Published workshop item " + item?.Title + " successfully.", Microsoft.Xna.Framework.Color.LightGreen); - - contentPackage.SteamWorkshopId = task.Result.FileId.Value; - //NOTE: This sets InstallTime one hour into the future to guarantee - //that the published content package won't be autoupdated incorrectly. - //Change if it causes issues. - contentPackage.InstallTime = DateTime.UtcNow + TimeSpan.FromHours(1); - contentPackage.Save(contentPackage.Path); - - SubscribeToWorkshopItem(task.Result.FileId); - } - - yield return CoroutineStatus.Success; - } - - /// - /// Forces a Workshop item to redownload. - /// - public static void ForceRedownload(Steamworks.Data.PublishedFileId itemId, Action onDownloadFinished = null) - { - Steamworks.Ugc.Item itemToNuke = new Steamworks.Ugc.Item(itemId); - string directory = itemToNuke.Directory; - if (Directory.Exists(directory)) - { - try - { - Directory.Delete(directory, true); - } - catch (Exception e) { DebugConsole.ThrowError("Failed to delete Workshop item cache", e); } - } - DebugConsole.NewMessage($"{itemToNuke.Download(onDownloadFinished, highPriority: true)}"); - } - - /// - /// Installs a workshop item by moving it to the game folder. - /// - public static bool InstallWorkshopItem(Steamworks.Ugc.Item? itemOrNull, out string errorMsg, bool enableContentPackage = false, bool suppressInstallNotif = false, Action onInstall = null) - { - errorMsg = "Item is null"; - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return false; } - if (!item.IsInstalled) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEnable", "[itemname]", item.Title); - DebugConsole.NewMessage(errorMsg, Color.Red); - return false; - } - - string metaDataFilePath = Path.Combine(item.Directory, MetadataFileName); - - if (!File.Exists(metaDataFilePath)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEnable", "[itemname]", item.Title); - DebugConsole.ThrowError(errorMsg); - return false; - } - - ContentPackage contentPackage = new ContentPackage(metaDataFilePath) - { - SteamWorkshopId = item.Id - }; - string newContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage); - - List existingPackages = ContentPackage.AllPackages.Where(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath()).ToList(); - if (existingPackages.Any()) - { - if (item.Owner.Id != Steamworks.SteamClient.SteamId) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorSamePathInstalled", - new string[] { "[itemname]", "[itempath]" }, - new string[] { item.Title, Path.GetDirectoryName(newContentPackagePath) }); - return false; - } - else - { - RemoveMods(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == contentPackage.SteamWorkshopId, - false); - } - } - - if (!contentPackage.IsCompatible()) - { - errorMsg = TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { contentPackage.Name, contentPackage.GameVersion.ToString(), GameMain.Version.ToString() }); - return false; - } - - Task newTask = null; - - lock (modCopiesInProgress) - { - if (modCopiesInProgress.ContainsKey(item.Id)) - { - errorMsg = ""; return true; - } - newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath); - modCopiesInProgress.Add(item.Id, newTask); - } - - TaskPool.Add("CopyWorkShopItemAsync", - newTask, - contentPackage, - (task, cp) => - { - try - { - if (task.IsFaulted || task.IsCanceled) - { - DebugConsole.ThrowError($"Failed to copy \"{item.Title}\"", task.Exception); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - task.TryGetResult(out string errorMsg); - if (!string.IsNullOrWhiteSpace(errorMsg)) - { - DebugConsole.ThrowError($"Failed to copy \"{item.Title}\": {errorMsg}"); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - - GameMain.Config.SuppressModFolderWatcher = true; - - var newPackage = new ContentPackage(cp.Path, newContentPackagePath) - { - SteamWorkshopId = item.Id, - InstallTime = item.Updated > item.Created ? item.Updated : item.Created - }; - - foreach (ContentFile contentFile in newPackage.Files) - { - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, cp, true); - } - - foreach (ContentFile file in existingPackages.SelectMany(p => p.Files)) - { - string path = CorrectContentFilePath(file.Path, file.Type, cp, true).CleanUpPath(); - if (newPackage.Files.Any(f => f.Path.CleanUpPath() == path)) { continue; } - newPackage.AddFile(path, file.Type); - } - - if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); - } - newPackage.Save(newContentPackagePath); - ContentPackage.AddPackage(newPackage); - - if (enableContentPackage) - { - if (newPackage.IsCorePackage) - { - GameMain.Config.SelectCorePackage(newPackage); - } - else - { - GameMain.Config.EnableRegularPackage(newPackage); - } - GameMain.Config.SaveNewPlayerConfig(); - - GameMain.Config.WarnIfContentPackageSelectionDirty(); - - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) - { - SubmarineInfo.RefreshSavedSubs(); - } - } - else if (!suppressInstallNotif) - { - GameMain.MainMenuScreen?.SetEnableModsNotification(true); - } - - GameMain.Config.SuppressModFolderWatcher = false; - - onInstall?.Invoke(newPackage); - - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); - } - catch - { - throw; - } - finally - { - modCopiesInProgress.Remove(item.Id); - } - }); - - errorMsg = ""; - return true; - } - - /// - /// Asynchronously copies a Workshop item into the Mods folder. - /// - /// Returns an empty string on success, otherwise returns an error message. - private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? itemOrNull, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath) - { - await Task.Yield(); - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return "Item is null"; } - - if (item.NeedsUpdate) - { - item.Download(highPriority: true); - await Task.Delay(1000); - } - while (item.NeedsUpdate && !item.IsDownloading && !item.IsDownloadPending && !item.IsInstalled) - { - if (!item.IsDownloading && !item.IsDownloadPending) - { - if (!item.Download()) - { - return TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item.Title); - } - } - await Task.Delay(1000); - } - - string targetPath = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(contentPackage)); - string copyingPath = Path.Combine(targetPath, CopyIndicatorFileName); - - string errorMsg = ""; - if (contentPackage.GameVersion > new Version(0, 9, 1, 0)) - { - Directory.CreateDirectory(targetPath); - File.WriteAllText(copyingPath, "TEMPORARY FILE"); - - SaveUtil.CopyFolder(item.Directory, targetPath, copySubDirs: true, overwriteExisting: false); - - File.Delete(copyingPath); - return ""; - } - - var allPackageFiles = Directory.GetFiles(item.Directory, "*", System.IO.SearchOption.AllDirectories); - List nonContentFiles = new List(); - foreach (string file in allPackageFiles) - { - if (file == metaDataFilePath) { continue; } - string relativePath = Path.GetRelativePath(item.Directory, file); - string fullPath = Path.GetFullPath(relativePath); - if (contentPackage.Files.Any(f => { string fp = Path.GetFullPath(f.Path); return fp == fullPath; })) { continue; } - nonContentFiles.Add(relativePath); - } - - /*if (File.Exists(newContentPackagePath) && !CheckFileEquality(newContentPackagePath, metaDataFilePath)) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); - DebugConsole.NewMessage(errorMsg, Color.Red); - return errorMsg; - } - - foreach (ContentFile contentFile in contentPackage.Files) - { - string sourceFile = Path.Combine(item?.Directory, contentFile.Path); - - if (File.Exists(sourceFile) && File.Exists(contentFile.Path) && !CheckFileEquality(sourceFile, contentFile.Path)) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); - DebugConsole.NewMessage(errorMsg, Color.Red); - return errorMsg; - } - }*/ - - Directory.CreateDirectory(targetPath); - File.WriteAllText(copyingPath, "TEMPORARY FILE"); - - foreach (ContentFile contentFile in contentPackage.Files) - { - contentFile.Path = contentFile.Path.CleanUpPathCrossPlatform(correctFilenameCase: true, item.Directory); - string sourceFile = Path.Combine(item.Directory, contentFile.Path); - if (!File.Exists(sourceFile)) - { - string[] splitPath = contentFile.Path.Split('/'); - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - sourceFile = Path.Combine(item.Directory, string.Join("/", splitPath.Skip(2))); - } - } - - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, contentPackage, - contentFile.Type != ContentType.Submarine); - - //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) - if (!ContentPackage.IsModFilePathAllowed(contentFile)) - { - //the content package is trying to copy a file to a prohibited path, which is not allowed - if (File.Exists(sourceFile)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorIllegalPathOnEnable", "[filename]", contentFile.Path); - return errorMsg; - } - //not trying to copy anything, so this is a reference to an external file - //if the external file doesn't exist, we cannot enable the package - else if (!File.Exists(contentFile.Path)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return errorMsg; - } - continue; - } - else if (!File.Exists(sourceFile)) - { - if (File.Exists(contentFile.Path)) - { - //the file is already present in the game folder, all good - continue; - } - else - { - //file not present in either the mod or the game folder -> cannot enable the package - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return errorMsg; - } - } - - //make sure the destination directory exists - Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); - CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: false); - } - - foreach (string nonContentFile in nonContentFiles) - { - string sourceFile = Path.Combine(item.Directory, nonContentFile); - if (!File.Exists(sourceFile)) { continue; } - string destinationPath = CorrectContentFilePath(nonContentFile, ContentType.None, contentPackage, false); - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: false); - } - - File.Delete(copyingPath); - return ""; - } - - private static void RemoveMods(Func predicate, bool delete = true) - { - var toRemoveCore = ContentPackage.CorePackages.Where(predicate).ToList(); - if (toRemoveCore.Contains(GameMain.Config.CurrentCorePackage)) { GameMain.Config.AutoSelectCorePackage(toRemoveCore); } - - var toRemoveRegular = ContentPackage.RegularPackages.Where(predicate).ToList(); - var packagesToDeselect = GameMain.Config.EnabledRegularPackages.Where(p => toRemoveRegular.Contains(p)).ToList(); - foreach (var cp in packagesToDeselect) - { - GameMain.Config.DisableRegularPackage(cp); - } - - if (delete) - { - var toRemove = toRemoveCore.Concat(toRemoveRegular); - foreach (var cp in toRemove) - { - try - { - string path = Path.GetDirectoryName(cp.Path); - if (Directory.Exists(path)) { Directory.Delete(path, true); } - } - catch (Exception e) - { - DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); - } - ContentPackage.RemovePackage(cp); - } - } - - GameMain.Config.SaveNewPlayerConfig(); - - GameMain.Config.WarnIfContentPackageSelectionDirty(); - } - - /// - /// Uninstalls a workshop item by removing the files from the game folder. - /// - public static bool UninstallWorkshopItem(Steamworks.Ugc.Item? item, bool noLog, out string errorMsg) - { - errorMsg = null; - if (!(item?.IsInstalled ?? false)) - { - errorMsg = "Cannot disable workshop item \"" + item?.Title + "\" because it has not been installed."; - if (!noLog) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - } - return false; - } - - ContentPackage contentPackage = new ContentPackage(Path.Combine(item?.Directory, MetadataFileName)) - { - SteamWorkshopId = item?.Id ?? 0 - }; - - GameMain.Config.SuppressModFolderWatcher = true; - try - { - RemoveMods(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == contentPackage.SteamWorkshopId); - } - catch (Exception e) - { - errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); - if (!noLog) - { - DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red); - } - return false; - } - GameMain.Config.SuppressModFolderWatcher = false; - - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, false, null); - - errorMsg = ""; - return true; - } - - /// - /// Is the item compatible with this version of Barotrauma. Returns null if compatibility couldn't be determined (item not installed) - /// - public static bool? CheckWorkshopItemCompatibility(Steamworks.Ugc.Item? item) - { - if (!(item?.IsInstalled ?? false)) { return null; } - - string metaDataPath = Path.Combine(item?.Directory, MetadataFileName); - if (!File.Exists(metaDataPath)) - { - DebugConsole.ThrowError("Metadata file for the Workshop item \"" + item?.Title + "\" not found. The file may be corrupted.", appendStackTrace: true); - return null; - } - - ContentPackage contentPackage = new ContentPackage(metaDataPath); - return contentPackage.IsCompatible(); - } - - public static bool CheckWorkshopItemInstalled(Steamworks.Ugc.Item? itemOrNull) - { - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return false; } - if (!item.IsInstalled) { return false; } - - lock (modCopiesInProgress) - { - if (modCopiesInProgress.ContainsKey(item.Id)) - { - return true; - } - } - - if (item.NeedsUpdate && !item.IsDownloading && !item.IsDownloadPending) - { - item.Download(); - return false; - } - if (!Directory.Exists(item.Directory)) - { - DebugConsole.ThrowError("Workshop item \"" + item.Title + "\" has been installed but the install directory cannot be found. Attempting to redownload..."); - item.Download(); - return false; - } - - string metaDataPath = ""; - try - { - metaDataPath = Path.Combine(item.Directory, MetadataFileName); - } - catch (ArgumentException) - { - string errorMessage = "Metadata file for the Workshop item \"" + item.Title + - "\" not found. Could not combine path (" + (item.Directory ?? "directory name empty") + ")."; - DebugConsole.ThrowError(errorMessage); - GameAnalyticsManager.AddErrorEventOnce("SteamManager.CheckWorkshopItemInstalled:PathCombineException" + item.Title, - GameAnalyticsManager.ErrorSeverity.Error, - "Metadata file for a Workshop item not found. Could not combine path."); - return false; - } - - if (!File.Exists(metaDataPath)) - { - DebugConsole.ThrowError("Metadata file for the Workshop item \"" + item.Title + "\" not found. The file may be corrupted."); - return false; - } - - ContentPackage contentPackage = new ContentPackage(metaDataPath) - { - SteamWorkshopId = item.Id - }; - //make sure the contentpackage file is present - if (!File.Exists(GetWorkshopItemContentPackagePath(contentPackage)) || - !ContentPackage.AllPackages.Any(cp => cp.SteamWorkshopId == contentPackage.SteamWorkshopId || - (cp.SteamWorkshopId == 0 && cp.Name == contentPackage.Name))) - { - return false; - } - - return true; - } - - public static bool CheckWorkshopItemUpToDate(Steamworks.Ugc.Item? itemOrNull) - { - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return false; } - if (!item.IsInstalled || item.NeedsUpdate || item.IsDownloading || item.IsDownloadPending) { return false; } - - string metaDataPath = Path.Combine(item.Directory, MetadataFileName); - if (!File.Exists(metaDataPath)) - { - DebugConsole.ThrowError("Metadata file for the Workshop item \"" + item.Title + "\" not found. The file may be corrupted."); - return false; - } - - ContentPackage steamPackage = new ContentPackage(metaDataPath) - { - SteamWorkshopId = item.Id - }; - ContentPackage myPackage = ContentPackage.AllPackages.FirstOrDefault(cp => cp.SteamWorkshopId == steamPackage.SteamWorkshopId); - - if (myPackage?.InstallTime == null) - { - return false; - } - DateTime latestTime = item.Updated > item.Created ? item.Updated : item.Created; - bool upToDate = latestTime <= myPackage.InstallTime.Value; - return upToDate; - } - - public static async Task AutoUpdateWorkshopItemsAsync() - { - await Task.Yield(); - - if (!isInitialized) { return false; } - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .WhereUserSubscribed() - .WithLongDescription(); - - List items = await GetWorkshopItemsAsync(query); - - GameMain.Config.SuppressModFolderWatcher = true; - - //remove mods that the player is no longer subscribed to - RemoveMods(cp => cp.SteamWorkshopId != 0 && !items.Any(it => it.Id == cp.SteamWorkshopId)); - - GameMain.Config.SuppressModFolderWatcher = false; - - - List updateNotifications = new List(); - foreach (var item in items) - { - try - { - if (!item.IsInstalled) { continue; } - - bool installedSuccessfully = false; - string errorMsg; - if (!CheckWorkshopItemInstalled(item)) - { - installedSuccessfully = InstallWorkshopItem(item, out errorMsg); - } - else if (!CheckWorkshopItemUpToDate(item)) - { - installedSuccessfully = UpdateWorkshopItem(item, out errorMsg); - } - else - { - continue; - } - - if (!installedSuccessfully) - { - CrossThread.RequestExecutionOnMainThread(() => - { - DebugConsole.NewMessage(errorMsg, Color.Red); - string errorId = errorMsg; - if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })) - { - UserData = errorId - }; - } - }); - } - else - { - updateNotifications.Add(TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); - } - } - catch (Exception e) - { - CrossThread.RequestExecutionOnMainThread(() => - { - string errorId = e.Message; - if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })) - { - UserData = errorId - }; - } - GameAnalyticsManager.AddErrorEventOnce( - "SteamManager.AutoUpdateWorkshopItems:" + e.Message, - GameAnalyticsManager.ErrorSeverity.Error, - "Failed to autoupdate workshop item. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - }); - } - } - - if (updateNotifications.Count > 0) - { - CrossThread.RequestExecutionOnMainThread(() => - { - while (updateNotifications.Count > 0) - { - float width = updateNotifications.Max(notif => GUI.Font.MeasureString(notif).X) * 1.25f; - - int notificationsPerMsgBox = 20; - new GUIMessageBox("", string.Join('\n', updateNotifications.Take(notificationsPerMsgBox)), - relativeSize: new Microsoft.Xna.Framework.Vector2(0.25f, 0.0f), - minSize: new Microsoft.Xna.Framework.Point((int)width, 0)); - updateNotifications.RemoveRange(0, Math.Min(notificationsPerMsgBox, updateNotifications.Count)); - } - }); - } - - List tasks; - lock (modCopiesInProgress) - { - tasks = modCopiesInProgress.Values.ToList(); - } - await Task.WhenAll(tasks); - - return true; - } - - public static bool UpdateWorkshopItem(Steamworks.Ugc.Item? item, out string errorMsg) - { - errorMsg = ""; - if (!(item?.IsInstalled ?? false)) { return false; } - bool reenable = GameMain.Config.AllEnabledPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == item?.Id); - if (item?.Owner.Id != Steamworks.SteamClient.SteamId) - { - if (!UninstallWorkshopItem(item, false, out errorMsg)) { return false; } - } - if (!InstallWorkshopItem(item, errorMsg: out errorMsg, enableContentPackage: reenable)) { return false; } - return true; - } - - private static string GetWorkshopItemContentPackagePath(ContentPackage contentPackage) - { - string packageName = contentPackage.Name.Trim(); - packageName = ToolBox.RemoveInvalidFileNameChars(packageName); - while (packageName.Last() == '.') { packageName = packageName.Substring(0, packageName.Length-1); } - //packageName = packageName + "_" + contentPackage.SteamWorkshopId.ToString(); - - return Path.Combine("Mods", packageName, MetadataFileName); - } - - private static void CorrectXMLFilePaths(ContentPackage package, XElement element) - { - foreach (var attr in element.Attributes()) - { - if ((attr.Name.ToString() == "file" || - attr.Name.ToString() == "folder" || - attr.Name.ToString() == "texture" || - attr.Name.ToString() == "monsterfile" || - attr.Name.ToString() == "characterfile") && - attr.Value.CleanUpPath().Contains("/")) - { - Enum.TryParse(attr.Name.LocalName, true, out ContentType type); - attr.Value = CorrectContentFilePath(attr.Value, type, package, true); - } - } - - foreach (var child in element.Elements()) - { - CorrectXMLFilePaths(package, child); - } - } - - private static void CorrectContentFileCopy(ContentPackage package, string src, string dest, bool overwrite) - { - if (!overwrite && File.Exists(dest)) { return; } - - if (Path.GetExtension(src).Equals(".xml", StringComparison.OrdinalIgnoreCase)) - { - XDocument doc = XMLExtensions.TryLoadXml(src); - if (doc != null) - { - CorrectXMLFilePaths(package, doc.Root); - using (System.IO.MemoryStream stream = new System.IO.MemoryStream()) - { - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings(); - settings.Indent = true; - settings.Encoding = new System.Text.UTF8Encoding(false); - using (var xmlWriter = System.Xml.XmlWriter.Create(stream, settings)) - { - doc.WriteTo(xmlWriter); - xmlWriter.Flush(); - string contents = System.Text.Encoding.UTF8.GetString(stream.ToArray()).Replace("\r\n", "\n"); - File.WriteAllText(dest, contents, System.Text.Encoding.UTF8); - } - } - } - else - { - File.Copy(src, dest, overwrite: true); - } - } - else - { - File.Copy(src, dest, overwrite: true); - } - } - - private static string CorrectContentFilePath(string contentFilePath, ContentType type, ContentPackage package, bool checkIfFileExists = false) - { - string packageName = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(package)); - - contentFilePath = contentFilePath.CleanUpPathCrossPlatform(); - - if (checkIfFileExists) - { - bool exists = File.Exists(contentFilePath); - if (type == ContentType.ServerExecutable) - { - exists |= File.Exists(Path.GetFileNameWithoutExtension(contentFilePath) + ".dll"); - } - if (exists) - { - return contentFilePath; - } - } - - string[] splitPath = contentFilePath.Split('/'); - if (splitPath.Length < 2 || splitPath[0] != "Mods" || splitPath[1] != packageName) - { - string newPath; - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - if (checkIfFileExists) - { - ContentPackage otherContentPackage = ContentPackage.AllPackages.FirstOrDefault(cp => cp.Name.Equals(splitPath[1], StringComparison.OrdinalIgnoreCase)); - if (otherContentPackage != null) - { - string otherPackageName = Path.GetDirectoryName(otherContentPackage.Path); - newPath = Path.Combine(otherPackageName, string.Join("/", splitPath.Skip(2))); - if (File.Exists(newPath)) - { - contentFilePath = newPath; - return contentFilePath; - } - } - } - splitPath = splitPath.Skip(Math.Clamp(splitPath.Length-1, 0, 2)).ToArray(); - newPath = Path.Combine(packageName, string.Join("/", splitPath)); - } - else - { - newPath = Path.Combine(packageName, contentFilePath); - } - contentFilePath = newPath; - } - - return contentFilePath.CleanUpPathCrossPlatform(false); - } - -#endregion - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index a32ef3bd0..98073f188 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -3,6 +3,7 @@ using Concentus.Structs; using Microsoft.Xna.Framework; using OpenAL; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; @@ -18,6 +19,9 @@ namespace Barotrauma.Networking private set; } + public static IReadOnlyList CaptureDeviceNames => + Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + private IntPtr captureDevice; private Thread captureThread; @@ -40,7 +44,7 @@ namespace Barotrauma.Networking public float Gain { - get { return GameMain.Config?.MicrophoneVolume ?? 1.0f; } + get { return GameSettings.CurrentConfig.Audio.MicrophoneVolume; } } public DateTime LastEnqueueAudio; @@ -106,16 +110,18 @@ namespace Barotrauma.Networking string errorCode = Alc.GetError(IntPtr.Zero).ToString(); if (!GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "capturedevicenotfound")) { - GUI.SettingsMenuOpen = false; + //GUI.SettingsMenuOpen = false; new GUIMessageBox(TextManager.Get("Error"), - (TextManager.Get("VoipCaptureDeviceNotFound", returnNull: true) ?? "Could not start voice capture, suitable capture device not found.") + " (" + errorCode + ")") + (TextManager.Get("VoipCaptureDeviceNotFound").Fallback("Could not start voice capture, suitable capture device not found.")) + " (" + errorCode + ")") { UserData = "capturedevicenotfound" }; } GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpenFailed", GameAnalyticsManager.ErrorSeverity.Error, "Alc.CaptureDeviceOpen(" + deviceName + ") failed. Error code: " + errorCode); - GameMain.Config.VoiceSetting = GameSettings.VoiceMode.Disabled; + var config = GameSettings.CurrentConfig; + config.Audio.VoiceSetting = VoiceMode.Disabled; + GameSettings.SetCurrentConfig(config); Instance?.Dispose(); Instance = null; return; @@ -157,13 +163,15 @@ namespace Barotrauma.Networking public static void ChangeCaptureDevice(string deviceName) { - GameMain.Config.VoiceCaptureDevice = deviceName; + var config = GameSettings.CurrentConfig; + config.Audio.VoiceCaptureDevice = deviceName; + GameSettings.SetCurrentConfig(config); if (Instance != null) { UInt16 storedBufferID = Instance.LatestBufferID; Instance.Dispose(); - Create(GameMain.Config.VoiceCaptureDevice, storedBufferID); + Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } } @@ -222,9 +230,9 @@ namespace Barotrauma.Networking LastAmplitude = maxAmplitude; bool allowEnqueue = overrideSound != null; - if (GameMain.WindowActive) + if (GameMain.WindowActive && SettingsMenu.Instance is null) { - ForceLocal = captureTimer > 0 ? ForceLocal : GameMain.Config.UseLocalVoiceByDefault; + ForceLocal = captureTimer > 0 ? ForceLocal : GameSettings.CurrentConfig.Audio.UseLocalVoiceByDefault; bool pttDown = false; if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && GUI.KeyboardDispatcher.Subscriber == null) @@ -239,14 +247,14 @@ namespace Barotrauma.Networking ForceLocal = false; } } - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Activity) { - if (dB > GameMain.Config.NoiseGateThreshold) + if (dB > GameSettings.CurrentConfig.Audio.NoiseGateThreshold) { allowEnqueue = true; } } - else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) + else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.PushToTalk) { if (pttDown) { @@ -277,7 +285,7 @@ namespace Barotrauma.Networking captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; if (allowEnqueue) { - captureTimer = GameMain.Config.VoiceChatCutoffPrevention; + captureTimer = GameSettings.CurrentConfig.Audio.VoiceChatCutoffPrevention; } prevCaptured = true; } @@ -378,6 +386,7 @@ namespace Barotrauma.Networking capturing = false; captureThread?.Join(); captureThread = null; + if (captureDevice != IntPtr.Zero) { Alc.CaptureCloseDevice(captureDevice); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 0d360bd80..85570fcbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -43,7 +43,7 @@ namespace Barotrauma.Networking public void SendToServer() { - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { if (VoipCapture.Instance != null) { @@ -54,8 +54,8 @@ namespace Barotrauma.Networking } else { - if (VoipCapture.Instance == null) VoipCapture.Create(GameMain.Config.VoiceCaptureDevice, storedBufferID); - if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) return; + if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) { return; } } if (DateTime.Now >= lastSendTime + VoipConfig.SEND_INTERVAL) @@ -80,7 +80,7 @@ namespace Barotrauma.Networking if (queue == null) { #if DEBUG - DebugConsole.NewMessage("Couldn't find VoipQueue with id " + queueId.ToString() + "!", GUI.Style.Red); + DebugConsole.NewMessage("Couldn't find VoipQueue with id " + queueId.ToString() + "!", GUIStyle.Red); #endif return; } @@ -102,7 +102,7 @@ namespace Barotrauma.Networking var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); - client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameMain.Config.DisableVoiceChatFilters; + client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; if (messageType == ChatMessageType.Radio) { client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range); @@ -111,7 +111,7 @@ namespace Barotrauma.Networking { client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange); } - if (messageType != ChatMessageType.Radio && Character.Controlled != null && !GameMain.Config.DisableVoiceChatFilters) + if (messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters) { client.VoipSound.UseMuffleFilter = SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRange, client.Character.CurrentHull); } @@ -144,7 +144,7 @@ namespace Barotrauma.Networking { if (voiceIconSheetRects == null) { - var soundIconStyle = GUI.Style.GetComponentStyle("GUISoundIcon"); + var soundIconStyle = GUIStyle.GetComponentStyle("GUISoundIcon"); Rectangle sourceRect = soundIconStyle.Sprites.First().Value.First().Sprite.SourceRect; var indexPieces = soundIconStyle.Element.Attribute("sheetindices").Value.Split(';'); voiceIconSheetRects = new Rectangle[indexPieces.Length]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 8d5406f96..23458baca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -122,7 +122,7 @@ namespace Barotrauma if (sub.EqualityCheckVal == 0) { //sub doesn't exist client-side, use hash to let the server know which one we voted for - msg.Write(sub.MD5Hash.Hash); + msg.Write(sub.MD5Hash.StringRepresentation); } break; case VoteType.Mode: @@ -302,7 +302,7 @@ namespace Barotrauma } else if (GameMain.Client.ConnectedClients.Count > 1) { - GameMain.NetworkMember.AddChatMessage(VotingInterface.GetSubmarineVoteResultMessage(subInfo, voteType, yesClientCount.ToString(), noClientCount.ToString(), passed), ChatMessageType.Server); + GameMain.NetworkMember.AddChatMessage(VotingInterface.GetSubmarineVoteResultMessage(subInfo, voteType, yesClientCount.ToString(), noClientCount.ToString(), passed).Value, ChatMessageType.Server); } if (passed) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 0f535c9d7..f0457e030 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Particles public float AngleMinRad { get; private set; } public float AngleMaxRad { get; private set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)] public float AngleMin { get => angleMin; @@ -28,7 +28,7 @@ namespace Barotrauma.Particles } } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)] public float AngleMax { get => angleMax; @@ -39,77 +39,77 @@ namespace Barotrauma.Particles } } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float DistanceMin { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float DistanceMax { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float VelocityMin { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float VelocityMax { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)] public float ScaleMin { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)] public float ScaleMax { get; set; } - [Editable(), Serialize("1,1", true)] + [Editable(), Serialize("1,1", IsPropertySaveable.Yes)] public Vector2 ScaleMultiplier { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)] public float EmitInterval { get; set; } - [Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, true, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")] + [Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, IsPropertySaveable.Yes, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")] public int ParticleAmount { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)] public float ParticlesPerSecond { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")] public float EmitAcrossRayInterval { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "Delay before the emitter becomes active after being created.")] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "Delay before the emitter becomes active after being created.")] public float InitialDelay { get; set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool HighQualityCollisionDetection { get; set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool CopyEntityAngle { get; set; } - [Editable, Serialize("1,1,1,1", true)] + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DrawOnTop { get; set; } - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Angle { get => AngleMin; set => AngleMin = AngleMax = value; } - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Distance { get => DistanceMin; set => DistanceMin = DistanceMax = value; } - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Velocity { get => VelocityMin; set => VelocityMin = VelocityMax = value; } - public Dictionary SerializableProperties { get; } + public Dictionary SerializableProperties { get; } public ParticleEmitterProperties(XElement element) { @@ -125,7 +125,7 @@ namespace Barotrauma.Particles public readonly ParticleEmitterPrefab Prefab; - public ParticleEmitter(XElement element) + public ParticleEmitter(ContentXElement element) { Prefab = new ParticleEmitterPrefab(element); } @@ -253,40 +253,24 @@ namespace Barotrauma.Particles class ParticleEmitterPrefab { - private string particlePrefabName; + private readonly Identifier particlePrefabName; - private ParticlePrefab particlePrefab; - public ParticlePrefab ParticlePrefab - { - get - { - if (particlePrefab == null && particlePrefabName != null) - { - particlePrefab = GameMain.ParticleManager?.FindPrefab(particlePrefabName); - if (particlePrefab == null) - { - DebugConsole.ThrowError($"Failed to find particle prefab \"{particlePrefabName}\"."); - particlePrefabName = null; - } - } - return particlePrefab; - } - } + public ParticlePrefab ParticlePrefab => ParticlePrefab.Prefabs[particlePrefabName]; public readonly ParticleEmitterProperties Properties; public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab.DrawOnTop; - public ParticleEmitterPrefab(XElement element) + public ParticleEmitterPrefab(ContentXElement element) { Properties = new ParticleEmitterProperties(element); - particlePrefabName = element.GetAttributeString("particle", ""); + particlePrefabName = element.GetAttributeIdentifier("particle", ""); } public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) { Properties = properties; - particlePrefab = prefab; + particlePrefabName = prefab.Identifier; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 97099d85f..558faee69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -44,8 +44,6 @@ namespace Barotrauma.Particles } private Particle[] particles; - public readonly PrefabCollection Prefabs = new PrefabCollection(); - private Camera cam; public Camera Camera @@ -58,62 +56,9 @@ namespace Barotrauma.Particles { this.cam = cam; - MaxParticles = GameMain.Config.ParticleLimit; + MaxParticles = GameSettings.CurrentConfig.Graphics.ParticleLimit; } - public void LoadPrefabs() - { - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) - { - LoadPrefabsFromFile(configFile); - } - } - - public void LoadPrefabsFromFile(ContentFile configFile) - { - var particleElements = new Dictionary(); - - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { return; } - - bool allowOverriding = false; - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - allowOverriding = true; - } - - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; - string name = element.Name.ToString().ToLowerInvariant(); - if (Prefabs.ContainsKey(name) || particleElements.ContainsKey(name)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing particle prefab '{name}' using the file '{configFile.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate particle prefab '{name}' found in '{configFile.Path}'! Each particle prefab must have a unique name. " + - "Use tags to override prefabs."); - continue; - } - } - particleElements.Add(name, element); - } - - foreach (var kvp in particleElements) - { - Prefabs.Add(new ParticlePrefab(kvp.Value, configFile), allowOverriding); - } - } - - public void RemovePrefabsByFile(string configFile) - { - Prefabs.RemoveByFile(configFile); - } public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess, collisionIgnoreTimer, tracerPoints: tracerPoints); @@ -179,12 +124,12 @@ namespace Barotrauma.Particles public List GetPrefabList() { - return Prefabs.ToList(); + return ParticlePrefab.Prefabs.ToList(); } public ParticlePrefab FindPrefab(string prefabName) { - return Prefabs.Find(p => p.Identifier.Equals(prefabName, StringComparison.OrdinalIgnoreCase)); + return ParticlePrefab.Prefabs.Find(p => p.Identifier == prefabName); } private void RemoveParticle(int index) @@ -211,7 +156,7 @@ namespace Barotrauma.Particles public void Update(float deltaTime) { - MaxParticles = GameMain.Config.ParticleLimit; + MaxParticles = GameSettings.CurrentConfig.Graphics.ParticleLimit; for (int i = 0; i < particleCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 9882048e8..de2a858cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -6,13 +6,15 @@ using System.Xml.Linq; namespace Barotrauma.Particles { - class ParticlePrefab : IPrefab, IDisposable, ISerializableEntity + class ParticlePrefab : Prefab, ISerializableEntity { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public enum DrawTargetType { Air = 1, Water = 2, Both = 3 } public readonly List Sprites; - public void Dispose() + public override void Dispose() { GameMain.ParticleManager?.RemoveByPrefab(this); foreach (Sprite spr in Sprites) @@ -22,51 +24,25 @@ namespace Barotrauma.Particles Sprites.Clear(); } - public string Name - { - get; - private set; - } - - public string FilePath - { - get; - private set; - } - - public string OriginalName { get { return Name; } } - - public string Identifier { get { return Name; } } - - public ContentPackage ContentPackage - { - get; - private set; - } - - public string DisplayName - { - get; - private set; - } - - [Editable(0.0f, float.MaxValue), Serialize(5.0f, false, description: "How many seconds the particle remains alive.")] + public string Name => Identifier.Value; + + [Editable(0.0f, float.MaxValue), Serialize(5.0f, IsPropertySaveable.No, description: "How many seconds the particle remains alive.")] public float LifeTime { get; private set; } - [Editable(0.0f, float.MaxValue), Serialize(0.0f, false, description: "Will randomize lifetime value between lifetime and lifetimeMin. If left to 0 will use only lifetime value.")] + [Editable(0.0f, float.MaxValue), Serialize(0.0f, IsPropertySaveable.No, description: "Will randomize lifetime value between lifetime and lifetimeMin. If left to 0 will use only lifetime value.")] public float LifeTimeMin { get; private set; } - [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMin { get; private set; } - [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMax { get; private set; } //movement ----------------------------------------- private float angularVelocityMin; public float AngularVelocityMinRad { get; private set; } - [Editable, Serialize(0.0f, false)] + [Editable, Serialize(0.0f, IsPropertySaveable.No)] public float AngularVelocityMin { get { return angularVelocityMin; } @@ -80,7 +56,7 @@ namespace Barotrauma.Particles private float angularVelocityMax; public float AngularVelocityMaxRad { get; private set; } - [Editable, Serialize(0.0f, false)] + [Editable, Serialize(0.0f, IsPropertySaveable.No)] public float AngularVelocityMax { get { return angularVelocityMax; } @@ -94,7 +70,7 @@ namespace Barotrauma.Particles private float startRotationMin; public float StartRotationMinRad { get; private set; } - [Editable, Serialize(0.0f, false, description: "The minimum initial rotation of the particle (in degrees).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The minimum initial rotation of the particle (in degrees).")] public float StartRotationMin { get { return startRotationMin; } @@ -108,7 +84,7 @@ namespace Barotrauma.Particles private float startRotationMax; public float StartRotationMaxRad { get; private set; } - [Editable, Serialize(0.0f, false, description: "The maximum initial rotation of the particle (in degrees).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The maximum initial rotation of the particle (in degrees).")] public float StartRotationMax { get { return startRotationMax; } @@ -119,19 +95,19 @@ namespace Barotrauma.Particles } } - [Editable, Serialize(false, false, description: "Should the particle face the direction it's moving towards.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle face the direction it's moving towards.")] public bool RotateToDirection { get; private set; } - [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.No, description: "Drag applied to the particle when it's moving through air.")] public float Drag { get; private set; } - [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.No, description: "Drag applied to the particle when it's moving through water.")] public float WaterDrag { get; private set; } private Vector2 velocityChange; public Vector2 VelocityChangeDisplay { get; private set; } - [Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the velocity of the particle changes per second.")] public Vector2 VelocityChange { get { return velocityChange; } @@ -145,7 +121,7 @@ namespace Barotrauma.Particles private Vector2 velocityChangeWater; public Vector2 VelocityChangeWaterDisplay { get; private set; } - [Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second when in water.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the velocity of the particle changes per second when in water.")] public Vector2 VelocityChangeWater { get { return velocityChangeWater; } @@ -156,78 +132,78 @@ namespace Barotrauma.Particles } } - [Editable(0.0f, 10000.0f), Serialize(0.0f, false, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")] + [Editable(0.0f, 10000.0f), Serialize(0.0f, IsPropertySaveable.No, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")] public float CollisionRadius { get; private set; } - [Editable, Serialize(false, false, description: "Does the particle collide with the walls of the submarine and the level.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the particle collide with the walls of the submarine and the level.")] public bool UseCollision { get; private set; } - [Editable, Serialize(false, false, description: "Does the particle disappear when it collides with something.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the particle disappear when it collides with something.")] public bool DeleteOnCollision { get; private set; } - [Editable(0.0f, 1.0f), Serialize(0.5f, false, description: "The friction coefficient of the particle, i.e. how much it slows down when it's sliding against a surface.")] + [Editable(0.0f, 1.0f), Serialize(0.5f, IsPropertySaveable.No, description: "The friction coefficient of the particle, i.e. how much it slows down when it's sliding against a surface.")] public float Friction { get; private set; } [Editable(0.0f, 1.0f)] - [Serialize(0.5f, false, description: "How much of the particle's velocity is conserved when it collides with something, i.e. the \"bounciness\" of the particle. (1.0 = the particle stops completely).")] + [Serialize(0.5f, IsPropertySaveable.No, description: "How much of the particle's velocity is conserved when it collides with something, i.e. the \"bounciness\" of the particle. (1.0 = the particle stops completely).")] public float Restitution { get; private set; } //size ----------------------------------------- - [Editable, Serialize("1.0,1.0", false, description: "The minimum initial size of the particle.")] + [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The minimum initial size of the particle.")] public Vector2 StartSizeMin { get; private set; } - [Editable, Serialize("1.0,1.0", false, description: "The maximum initial size of the particle.")] + [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The maximum initial size of the particle.")] public Vector2 StartSizeMax { get; private set; } - - [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMin { get; private set; } - [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMax { get; private set; } - [Editable, Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How many seconds it takes for the particle to grow to it's initial size.")] public float GrowTime { get; private set; } //rendering ----------------------------------------- - [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No, description: "The initial color of the particle.")] public Color StartColor { get; private set; } - [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No, description: "The initial color of the particle.")] public Color MiddleColor { get; private set; } - [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The color of the particle at the end of its lifetime.")] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No, description: "The color of the particle at the end of its lifetime.")] public Color EndColor { get; private set; } - [Editable, Serialize(false, false, description: "If true the color will go from StartColor to EndcColor and back to StartColor.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "If true the color will go from StartColor to EndcColor and back to StartColor.")] public bool UseMiddleColor { get; private set; } - [Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")] + [Editable, Serialize(DrawTargetType.Air, IsPropertySaveable.No, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } - [Editable, Serialize(false, false, description: "Should the particle be always rendered on top of entities?")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle be always rendered on top of entities?")] public bool DrawOnTop { get; private set; } - [Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")] + [Editable, Serialize(ParticleBlendState.AlphaBlend, IsPropertySaveable.No, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } - [Editable, Serialize(0, false, description: "Particles with a higher priority can replace lower-priority ones if the maximum number of active particles has been reached.")] + [Editable, Serialize(0, IsPropertySaveable.No, description: "Particles with a higher priority can replace lower-priority ones if the maximum number of active particles has been reached.")] public int Priority { get; private set; } //animation ----------------------------------------- - [Editable(0.0f, float.MaxValue), Serialize(1.0f, false, description: "The duration of the particle's animation cycle (if it's animated).")] + [Editable(0.0f, float.MaxValue), Serialize(1.0f, IsPropertySaveable.No, description: "The duration of the particle's animation cycle (if it's animated).")] public float AnimDuration { get; private set; } - [Editable, Serialize(true, false, description: "Should the sprite animation be looped, or stay at the last frame when the animation finishes.")] + [Editable, Serialize(true, IsPropertySaveable.No, description: "Should the sprite animation be looped, or stay at the last frame when the animation finishes.")] public bool LoopAnim { get; private set; } //---------------------------------------------------- public readonly List SubEmitters = new List(); - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -235,18 +211,13 @@ namespace Barotrauma.Particles //---------------------------------------------------- - public ParticlePrefab(XElement element, ContentFile file) + public ParticlePrefab(ContentXElement element, ContentFile file) : base(file, element.NameAsIdentifier()) { - Name = element.Name.ToString(); - FilePath = file.Path; - ContentPackage = file.ContentPackage; - DisplayName = TextManager.Get("particle." + Name, true) ?? Name; - Sprites = new List(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index 989e459a4..f14b1e743 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -66,7 +66,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(pos.X - 5, -(pos.Y + 5)), - Vector2.One * 10.0f, GUI.Style.Red, false, 0, 3); + Vector2.One * 10.0f, GUIStyle.Red, false, 0, 3); } if (drawOffset != Vector2.Zero) diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index d2ca4d04b..3aedb98d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -24,11 +24,11 @@ namespace Barotrauma public class KeyOrMouse { - public Keys Key { get; private set; } + public readonly Keys Key; - private string name; + private LocalizedString name; - public string Name + public LocalizedString Name { get { @@ -39,6 +39,9 @@ namespace Barotrauma public MouseButton MouseButton { get; private set; } + public static implicit operator KeyOrMouse(Keys key) { return new KeyOrMouse(key); } + public static implicit operator KeyOrMouse(MouseButton mouseButton) { return new KeyOrMouse(mouseButton); } + public KeyOrMouse(Keys keyBinding) { this.Key = keyBinding; @@ -47,6 +50,7 @@ namespace Barotrauma public KeyOrMouse(MouseButton mouseButton) { + this.Key = Keys.None; this.MouseButton = mouseButton; } @@ -112,14 +116,7 @@ namespace Barotrauma { if (obj is KeyOrMouse keyOrMouse) { - if (MouseButton != MouseButton.None) - { - return keyOrMouse.MouseButton == MouseButton; - } - else - { - return keyOrMouse.Key.Equals(Key); - } + return this == keyOrMouse; } else { @@ -127,6 +124,68 @@ namespace Barotrauma } } + public static bool operator ==(KeyOrMouse a, KeyOrMouse b) + { + if (a is null) + { + return b is null; + } + else if (a.MouseButton != MouseButton.None) + { + return a.MouseButton == b.MouseButton; + } + else + { + return a.Key.Equals(b.Key); + } + } + + public static bool operator !=(KeyOrMouse a, KeyOrMouse b) + { + return !(a == b); + } + + public static bool operator ==(KeyOrMouse keyOrMouse, Keys key) + { + if (keyOrMouse.MouseButton != MouseButton.None) { return false; } + return keyOrMouse.Key == key; + } + + public static bool operator !=(KeyOrMouse keyOrMouse, Keys key) + { + return !(keyOrMouse == key); + } + + public static bool operator ==(Keys key, KeyOrMouse keyOrMouse) + { + return keyOrMouse == key; + } + + public static bool operator !=(Keys key, KeyOrMouse keyOrMouse) + { + return keyOrMouse != key; + } + + public static bool operator ==(KeyOrMouse keyOrMouse, MouseButton mb) + { + return keyOrMouse.MouseButton == mb && keyOrMouse.Key == Keys.None; + } + + public static bool operator !=(KeyOrMouse keyOrMouse, MouseButton mb) + { + return !(keyOrMouse == mb); + } + + public static bool operator ==(MouseButton mb, KeyOrMouse keyOrMouse) + { + return keyOrMouse == mb; + } + + public static bool operator !=(MouseButton mb, KeyOrMouse keyOrMouse) + { + return keyOrMouse != mb; + } + public override string ToString() { switch (MouseButton) @@ -146,7 +205,7 @@ namespace Barotrauma return hashCode; } - public string GetName() + public LocalizedString GetName() { if (PlayerInput.NumberKeys.Contains(Key)) { @@ -196,10 +255,11 @@ namespace Barotrauma #if WINDOWS [DllImport("user32.dll")] static extern int GetSystemMetrics(int smIndex); + private const int SM_SWAPBUTTON = 23; public static bool MouseButtonsSwapped() { - return GetSystemMetrics(23) != 0; //SM_SWAPBUTTON + return GetSystemMetrics(SM_SWAPBUTTON) != 0; } #else public static bool MouseButtonsSwapped() @@ -428,17 +488,17 @@ namespace Barotrauma public static bool KeyHit(InputType inputType) { - return AllowInput && GameMain.Config.KeyBind(inputType).IsHit(); + return AllowInput && GameSettings.CurrentConfig.KeyMap.Bindings[inputType].IsHit(); } public static bool KeyDown(InputType inputType) { - return AllowInput && GameMain.Config.KeyBind(inputType).IsDown(); + return AllowInput && GameSettings.CurrentConfig.KeyMap.Bindings[inputType].IsDown(); } public static bool KeyUp(InputType inputType) { - return AllowInput && !GameMain.Config.KeyBind(inputType).IsDown(); + return AllowInput && !GameSettings.CurrentConfig.KeyMap.Bindings[inputType].IsDown(); } public static bool KeyHit(Keys button) @@ -449,7 +509,7 @@ namespace Barotrauma public static bool InventoryKeyHit(int index) { if (index == -1) return false; - return AllowInput && GameMain.Config.InventoryKeyBind(index).IsHit(); + return AllowInput && GameSettings.CurrentConfig.InventoryKeyMap.Bindings[index].IsHit(); } public static bool KeyDown(Keys button) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index cbc8188bf..436132dba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -104,9 +104,7 @@ namespace Barotrauma try { string exePath = System.Reflection.Assembly.GetEntryAssembly().Location; - var md5 = System.Security.Cryptography.MD5.Create(); - byte[] exeBytes = File.ReadAllBytes(exePath); - exeHash = new Md5Hash(exeBytes); + exeHash = Md5Hash.CalculateForFile(exePath, Md5Hash.StringHashOptions.BytePerfect); } catch { @@ -125,19 +123,17 @@ namespace Barotrauma { //exception occurred in loading screen: //assume content packages are the culprit and reset them - XDocument doc = XMLExtensions.TryLoadXml(GameSettings.PlayerSavePath); - XDocument baseDoc = XMLExtensions.TryLoadXml(GameSettings.SavePath); - if (doc != null && baseDoc != null) + XDocument doc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); + if (doc?.Root != null) { XElement newElement = new XElement(doc.Root.Name); newElement.Add(doc.Root.Attributes()); - string[] contentPackageTags = { "contentpackage", "contentpackages" }; - bool elementNameMatches(XElement element) - => contentPackageTags.Any(t => element.Name.LocalName.Equals(t, StringComparison.InvariantCultureIgnoreCase)); - newElement.Add(doc.Root.Elements().Where(e => !elementNameMatches(e))); - newElement.Add(baseDoc.Root.Elements().Where(e => elementNameMatches(e))); + Identifier[] contentPackageTags = { "contentpackage".ToIdentifier(), "contentpackages".ToIdentifier() }; + newElement.Add(doc.Root.Elements().Where(e => !contentPackageTags.Contains(e.NameAsIdentifier()))); + newElement.Add(new XElement("core", + new XAttribute("path", ContentPackageManager.VanillaFileList))); XDocument newDoc = new XDocument(newElement); - newDoc.Save(GameSettings.PlayerSavePath); + newDoc.Save(GameSettings.PlayerConfigPath); sb.AppendLine("To prevent further startup errors, installed mods will be disabled the next time you launch the game."); sb.AppendLine("\n"); } @@ -148,22 +144,19 @@ namespace Barotrauma //welp i guess we couldn't reset the config! } - if (exeHash?.Hash != null) + if (exeHash?.StringRepresentation != null) { - sb.AppendLine(exeHash.Hash); + sb.AppendLine(exeHash.StringRepresentation); } sb.AppendLine("\n"); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); - if (GameMain.Config != null) + sb.AppendLine($"Graphics mode: {GameSettings.CurrentConfig.Graphics.Width}x{GameSettings.CurrentConfig.Graphics.Height} ({GameSettings.CurrentConfig.Graphics.DisplayMode})"); + sb.AppendLine("VSync " + (GameSettings.CurrentConfig.Graphics.VSync ? "ON" : "OFF")); + sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); + if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Graphics mode: " + GameMain.Config.GraphicsWidth + "x" + GameMain.Config.GraphicsHeight + " (" + GameMain.Config.WindowMode.ToString() + ")"); - sb.AppendLine("VSync " + (GameMain.Config.VSyncEnabled ? "ON" : "OFF")); - sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); - if (GameMain.Config.AllEnabledPackages != null) - { - sb.AppendLine("Selected content packages: " + (!GameMain.Config.AllEnabledPackages.Any() ? "None" : string.Join(", ", GameMain.Config.AllEnabledPackages.Select(c => c.Name)))); - } + sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); @@ -246,7 +239,12 @@ namespace Barotrauma if (GameAnalyticsManager.SendUserStatistics) { //send crash report before appending debug console messages (which may contain non-anonymous information) - GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, sb.ToString()); + string crashHeader = exception.Message; + if (exception.TargetSite != null) + { + crashHeader += " " + exception.TargetSite.ToString(); + } + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, crashHeader + "\n\n" + sb.ToString()); GameAnalyticsManager.ShutDown(); } @@ -260,8 +258,9 @@ namespace Barotrauma File.WriteAllText(filePath, crashReport); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } - + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameAnalyticsManager.SendUserStatistics) { CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers.", filePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index cf7190c75..17e268d1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -15,7 +15,7 @@ namespace Barotrauma public Action OnFinished; - private string textOverlay; + private LocalizedString textOverlay; private float textOverlayTimer; private Vector2 textOverlaySize; @@ -43,15 +43,15 @@ namespace Barotrauma { base.Select(); - textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUI.Font); - textOverlaySize = GUI.Font.MeasureString(textOverlay); + textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUIStyle.Font); + textOverlaySize = GUIStyle.Font.MeasureString(textOverlay); textOverlayTimer = 0.0f; video = Video.Load(GameMain.GraphicsDeviceManager.GraphicsDevice, GameMain.SoundManager, "Content/SplashScreens/Ending.webm"); video.Play(); creditsPlayer.Restart(); creditsPlayer.Visible = false; - SteamAchievementManager.UnlockAchievement("campaigncompleted", unlockClients: true); + SteamAchievementManager.UnlockAchievement("campaigncompleted".ToIdentifier(), unlockClients: true); } public override void Deselect() @@ -59,7 +59,7 @@ namespace Barotrauma video?.Dispose(); video = null; GUI.HideCursor = false; - SoundPlayer.OverrideMusicType = null; + SoundPlayer.OverrideMusicType = Identifier.Empty; } public override void Update(double deltaTime) @@ -67,7 +67,7 @@ namespace Barotrauma if (creditsPlayer.Finished) { OnFinished?.Invoke(); - SoundPlayer.OverrideMusicType = null; + SoundPlayer.OverrideMusicType = Identifier.Empty; } } @@ -82,7 +82,7 @@ namespace Barotrauma } else { - SoundPlayer.OverrideMusicType = "ending"; + SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); float duration = 20.0f; float creditsDelay = 3.0f; if (textOverlayTimer < duration + creditsDelay) @@ -102,7 +102,7 @@ namespace Barotrauma { textAlpha = 1.0f; } - GUI.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); + GUIStyle.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 2bc4bee2b..e93563192 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -22,13 +22,13 @@ namespace Barotrauma }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) { textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); GUIFrame radiationBoxContainer @@ -36,7 +36,7 @@ namespace Barotrauma GUITickBox radiationEnabledTickBox = null; if (MapGenerationParams.Instance.RadiationParams != null) { - radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), radiationBoxContainer.RectTransform, Anchor.Center), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), radiationBoxContainer.RectTransform, Anchor.Center), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) { Selected = true, OnSelected = box => true @@ -44,8 +44,8 @@ namespace Barotrauma } var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true) - { + var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true) + { ToolTip = TextManager.Get("maxmissioncounttooltip") }; int maxMissionCount = GameMain.NetworkMember.ServerSettings.MaxMissionCount; @@ -91,7 +91,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) { - saveNameBox.Flash(GUI.Style.Red); + saveNameBox.Flash(GUIStyle.Red); return false; } @@ -106,7 +106,7 @@ namespace Barotrauma return false; } - if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) + if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) { new GUIMessageBox(TextManager.Get("error"), TextManager.Get("nohashsubmarineselected")); return false; @@ -127,7 +127,7 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => @@ -147,7 +147,7 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), TextManager.Get("ShuttleWarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (button, obj) => { @@ -173,7 +173,7 @@ namespace Barotrauma StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUI.Style.SmallFont, textColor: GUI.Style.Green) + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) { TextGetter = () => { @@ -195,8 +195,8 @@ namespace Barotrauma private IEnumerable WaitForCampaignSetup() { GUI.SetCursorWaiting(); - string headerText = TextManager.Get("CampaignStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("CampaignStarting"), new string[] { TextManager.Get("Cancel") }); + var headerText = TextManager.Get("CampaignStartingPleaseWait"); + var msgBox = new GUIMessageBox(headerText, TextManager.Get("CampaignStarting"), new LocalizedString[] { TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { @@ -270,7 +270,8 @@ namespace Barotrauma prevSaveFiles?.Add(saveFile); string[] splitSaveFile = saveFile.Split(';'); saveFrame.UserData = splitSaveFile[0]; - fileName = nameText.Text = Path.GetFileNameWithoutExtension(splitSaveFile[0]); + fileName = Path.GetFileNameWithoutExtension(splitSaveFile[0]); + nameText.Text = fileName; if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } @@ -283,27 +284,27 @@ namespace Barotrauma if (!string.IsNullOrEmpty(contentPackageStr)) { List contentPackagePaths = contentPackageStr.Split('|').ToList(); - if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out string errorMsg)) + if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out LocalizedString errorMsg)) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); } } if (!isCompatible) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: subName, font: GUI.SmallFont) + text: subName, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTime, textAlignment: Alignment.Right, font: GUI.SmallFont) + text: saveTime, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName @@ -373,8 +374,8 @@ namespace Barotrauma string saveFile = obj as string; if (obj == null) { return false; } - string header = TextManager.Get("deletedialoglabel"); - string body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); + var header = TextManager.Get("deletedialoglabel"); + var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); EventEditorScreen.AskForConfirmation(header, body, () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 2ce5f1b82..9e15a34c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -134,16 +134,16 @@ namespace Barotrauma columnContainer.Recalculate(); // New game left side - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont); saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) { textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUI.SubHeadingFont); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUIStyle.SubHeadingFont); var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); moddedDropdown.AddItem(TextManager.Get("clientpermission.all"), CategoryFilter.All); @@ -155,11 +155,11 @@ namespace Barotrauma { Stretch = true }; - + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; @@ -187,7 +187,7 @@ namespace Barotrauma RelativeSpacing = 0.025f }; - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1f), firstPageButtonContainer.RectTransform), "", font: GUI.Style.Font, textColor: GUI.Style.Green, textAlignment: Alignment.CenterLeft) + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1f), firstPageButtonContainer.RectTransform), "", font: GUIStyle.Font, textColor: GUIStyle.Green, textAlignment: Alignment.CenterLeft) { TextGetter = () => { @@ -200,7 +200,7 @@ namespace Barotrauma return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); } }; - + CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), firstPageButtonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) { OnClicked = (tb, userdata) => @@ -218,7 +218,7 @@ namespace Barotrauma return false; } }; - + var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") { IgnoreLayoutGroups = true, @@ -238,8 +238,8 @@ namespace Barotrauma secondPageLayout.RelativeSpacing = 0.01f; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), - TextManager.Get("Crew"), font: GUI.Style.SubHeadingFont, textAlignment: Alignment.TopLeft); - + TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft); + characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) { Stretch = true, @@ -266,7 +266,7 @@ namespace Barotrauma OnClicked = FinishSetup }; } - + public void RandomizeCrew() { var characterInfos = new List<(CharacterInfo Info, JobPrefab Job)>(); @@ -275,9 +275,10 @@ namespace Barotrauma for (int i = 0; i < jobPrefab.InitialCount; i++) { var variant = Rand.Range(0, jobPrefab.Variants); - characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant), jobPrefab)); + characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant), jobPrefab)); } } + characterInfos.Sort((a, b) => Math.Sign(b.Job.MinKarma - a.Job.MinKarma)); characterInfoColumns.ClearChildren(); CharacterMenus?.ForEach(m => m.Dispose()); @@ -344,7 +345,7 @@ namespace Barotrauma private void CreateCustomizeWindow() { - CampaignCustomizeSettings = new GUIMessageBox("", "", new string[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); + CampaignCustomizeSettings = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) @@ -355,7 +356,7 @@ namespace Barotrauma if (MapGenerationParams.Instance.RadiationParams != null) { bool prevRadiationToggleEnabled = EnableRadiationToggle?.Selected ?? true; - EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) { Selected = prevRadiationToggleEnabled, ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") @@ -366,25 +367,25 @@ namespace Barotrauma Stretch = true, ToolTip = TextManager.Get("maxmissioncounttooltip") }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true); + var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true); var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; var maxMissionCountButtons = new GUIButton[2]; maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") { OnClicked = (button, obj) => { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); return true; } }; - string prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); + RichString prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); MaxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), prevMaxMissionCountText, textAlignment: Alignment.Center, style: "GUITextBox"); maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") { OnClicked = (button, obj) => { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); return true; } }; @@ -405,10 +406,10 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) { - saveNameBox.Flash(GUI.Style.Red); + saveNameBox.Flash(GUIStyle.Red); return false; } - + SubmarineInfo selectedSub = null; if (!(subList.SelectedData is SubmarineInfo)) { return false; } @@ -420,7 +421,7 @@ namespace Barotrauma return false; } - if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) + if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) { ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; subList.SelectedComponent.CanBeFocused = false; @@ -433,7 +434,7 @@ namespace Barotrauma CampaignSettings settings = new CampaignSettings(); settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; - if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text, out int missionCount)) + if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text.SanitizedValue, out int missionCount)) { settings.MaxMissionCount = missionCount; } @@ -448,8 +449,8 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => { @@ -467,7 +468,7 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), TextManager.Get("ShuttleWarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (button, obj) => { @@ -499,7 +500,7 @@ namespace Barotrauma { var sub = child.UserData as SubmarineInfo; if (sub == null) { return; } - child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.ToLower().Contains(filter.ToLower()); + child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } } @@ -522,7 +523,7 @@ namespace Barotrauma sub.CreatePreviewWindow(subPreviewContainer); return true; } - + public void CreateDefaultSaveName() { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer); @@ -555,7 +556,7 @@ namespace Barotrauma { var textBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), style: "ListBoxElement") + ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), style: "ListBoxElement") { ToolTip = sub.Description, UserData = sub @@ -564,13 +565,13 @@ namespace Barotrauma if (!sub.RequiredContentPackagesInstalled) { textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); - textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.RawToolTip; + textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.ToolTip.SanitizedString; } var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CampaignMode.InitialMoney ? GUI.Style.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CampaignMode.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; #if !DEBUG @@ -629,7 +630,7 @@ namespace Barotrauma { new GUIMessageBox( TextManager.Get("error"), - TextManager.GetWithVariables("showinfoldererror", new string[] { "[folder]", "[errormessage]" }, new string[] { SaveUtil.SaveFolder, e.Message })); + TextManager.GetWithVariables("showinfoldererror", ("[folder]", SaveUtil.SaveFolder), ("[errormessage]", e.Message))); } return true; } @@ -653,14 +654,14 @@ namespace Barotrauma bool isCompatible = true; prevSaveFiles ??= new List(); - + nameText.Text = Path.GetFileNameWithoutExtension(saveFile); XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); if (doc?.Root == null) { DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; continue; } if (doc.Root.GetChildElement("multiplayercampaign") != null) @@ -672,7 +673,7 @@ namespace Barotrauma subName = doc.Root.GetAttributeString("submarine", ""); saveTime = doc.Root.GetAttributeString("savetime", ""); isCompatible = SaveUtil.IsSaveFileCompatible(doc); - contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); + contentPackageStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", ""); prevSaveFiles?.Add(saveFile); if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) { @@ -682,27 +683,27 @@ namespace Barotrauma if (!string.IsNullOrEmpty(contentPackageStr)) { List contentPackagePaths = contentPackageStr.Split('|').ToList(); - if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out string errorMsg)) + if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out LocalizedString errorMsg)) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); } } if (!isCompatible) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: subName, font: GUI.SmallFont) + text: subName, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTime, textAlignment: Alignment.Right, font: GUI.SmallFont) + text: saveTime, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName @@ -782,8 +783,8 @@ namespace Barotrauma var titleText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0, 0.05f) - }, - Path.GetFileNameWithoutExtension(fileName), font: GUI.LargeFont, textAlignment: Alignment.Center); + }, + Path.GetFileNameWithoutExtension(fileName), font: GUIStyle.LargeFont, textAlignment: Alignment.Center); titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width); var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) @@ -791,9 +792,9 @@ namespace Barotrauma RelativeOffset = new Vector2(0, 0.1f) }); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUI.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUI.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUI.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) { @@ -812,8 +813,8 @@ namespace Barotrauma string saveFile = obj as string; if (obj == null) { return false; } - string header = TextManager.Get("deletedialoglabel"); - string body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); + LocalizedString header = TextManager.Get("deletedialoglabel"); + LocalizedString body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); EventEditorScreen.AskForConfirmation(header, body, () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index e8bf31049..7de7bbc32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -115,7 +115,7 @@ namespace Barotrauma RelativeSpacing = 0.05f, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), repairContent.RectTransform), "", font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), repairContent.RectTransform), "", font: GUIStyle.LargeFont) { TextGetter = GetMoney }; @@ -132,11 +132,11 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - var repairHullsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("RepairAllWalls"), textAlignment: Alignment.Right, font: GUI.SubHeadingFont) + var repairHullsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("RepairAllWalls"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), CampaignMode.HullRepairCost.ToString(), textAlignment: Alignment.Right, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), CampaignMode.HullRepairCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); repairHullsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairHullsHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("Repair")) { OnClicked = (btn, userdata) => @@ -178,11 +178,11 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - var repairItemsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("RepairAllItems"), textAlignment: Alignment.Right, font: GUI.SubHeadingFont) + var repairItemsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("RepairAllItems"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), CampaignMode.ItemRepairCost.ToString(), textAlignment: Alignment.Right, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), CampaignMode.ItemRepairCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); repairItemsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairItemsHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("Repair")) { OnClicked = (btn, userdata) => @@ -224,11 +224,11 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - var replaceShuttlesLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUI.SubHeadingFont) + var replaceShuttlesLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), CampaignMode.ShuttleReplaceCost.ToString(), textAlignment: Alignment.Right, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), CampaignMode.ShuttleReplaceCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); replaceShuttlesButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), replaceShuttlesHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("ReplaceShuttles")) { OnClicked = (btn, userdata) => @@ -404,11 +404,11 @@ namespace Barotrauma RelativeSpacing = 0.02f, }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) { AutoScaleHorizontal = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); Sprite portrait = location.Type.GetPortrait(location.PortraitId); portrait.EnsureLazyLoaded(); @@ -429,11 +429,11 @@ namespace Barotrauma if (connection?.LevelData != null) { var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - TextManager.Get("Biome", fallBackTag: "location"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), connection.Biome.DisplayName, textAlignment: Alignment.CenterRight); var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); if (connection.LevelData.HasBeaconStation) @@ -448,7 +448,7 @@ namespace Barotrauma ToolTip = TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip") }; new GUITextBlock(new RectTransform(Vector2.One, beaconStationContent.RectTransform), - TextManager.Get("submarinetype.beaconstation", fallBackTag: "beaconstationsonarlabel"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + TextManager.Get("submarinetype.beaconstation", "beaconstationsonarlabel"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { Padding = Vector4.Zero, ToolTip = icon.ToolTip @@ -465,7 +465,7 @@ namespace Barotrauma ToolTip = TextManager.Get("HuntingGroundsTooltip") }; new GUITextBlock(new RectTransform(Vector2.One, huntingGroundsContent.RectTransform), - TextManager.Get("missionname.huntinggrounds"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + TextManager.Get("missionname.huntinggrounds"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { Padding = Vector4.Zero, ToolTip = icon.ToolTip @@ -513,7 +513,7 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(5) }; - var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUI.SubHeadingFont, wrap: true); + var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUIStyle.SubHeadingFont, wrap: true); // missionName.RectTransform.MinSize = new Point(0, (int)(missionName.Rect.Height * 1.5f)); if (mission != null) { @@ -541,7 +541,7 @@ namespace Barotrauma foreach (GUITextBlock rewardText in missionRewardTexts) { Mission otherMission = rewardText.UserData as Mission; - rewardText.SetRichText(otherMission.GetMissionRewardText(Submarine.MainSub)); + rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); } UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); @@ -586,17 +586,17 @@ namespace Barotrauma //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(10)) }, style: null); - - var rewardText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.GetMissionRewardText(Submarine.MainSub), wrap: true, parseRichText: true) + + var rewardText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(mission.GetMissionRewardText(Submarine.MainSub)), wrap: true) { UserData = mission }; missionRewardTexts.Add(rewardText); - string reputationText = mission.GetReputationRewardText(mission.Locations[0]); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true, parseRichText: true); + LocalizedString reputationText = mission.GetReputationRewardText(mission.Locations[0]); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.Description, wrap: true, parseRichText: true); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(mission.Description), wrap: true); } missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(0)); foreach (GUIComponent child in missionTextContent.Children) @@ -636,7 +636,7 @@ namespace Barotrauma var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), isHorizontal: true); - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), buttonArea.RectTransform), "", font: GUI.Style.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), buttonArea.RectTransform), "", font: GUIStyle.SubHeadingFont) { TextGetter = () => { @@ -652,7 +652,7 @@ namespace Barotrauma if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && missionList.Content.Children.Any(c => c.UserData is Mission)) { - var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => { StartRound?.Invoke(); @@ -740,7 +740,7 @@ namespace Barotrauma } } - public static string GetMoney() + public static LocalizedString GetMoney() { return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Money)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 41b8e927c..1556924c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -16,7 +16,7 @@ using Barotrauma.IO; namespace Barotrauma.CharacterEditor { - class CharacterEditorScreen : Screen + class CharacterEditorScreen : EditorScreen { public static CharacterEditorScreen Instance { get; private set; } @@ -142,14 +142,14 @@ namespace Barotrauma.CharacterEditor Submarine.MainSub.GodMode = true; if (Character.Controlled == null) { - var humanConfig = CharacterPrefab.HumanConfigFile; - if (string.IsNullOrEmpty(humanConfig)) + var humanSpeciesName = CharacterPrefab.HumanSpeciesName; + if (humanSpeciesName.IsEmpty) { - SpawnCharacter(AllFiles.First()); + SpawnCharacter(AllSpecies.First()); } else { - SpawnCharacter(humanConfig); + SpawnCharacter(humanSpeciesName); } } else @@ -162,7 +162,7 @@ namespace Barotrauma.CharacterEditor GameMain.Instance.ResolutionChanged += OnResolutionChanged; Instance = this; - if (!GameMain.Config.EditorDisclaimerShown) + if (!GameSettings.CurrentConfig.EditorDisclaimerShown) { GameMain.Instance.ShowEditorDisclaimer(); } @@ -199,7 +199,7 @@ namespace Barotrauma.CharacterEditor jointEndLimb = null; anchor1Pos = null; jointStartLimb = null; - allFiles = null; + allSpecies = null; onlyShowSourceRectForSelectedLimbs = false; unrestrictSpritesheet = false; editedCharacters.Clear(); @@ -246,8 +246,8 @@ namespace Barotrauma.CharacterEditor public override void Deselect() { base.Deselect(); - SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume, 0); + SoundPlayer.OverrideMusicType = Identifier.Empty; + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); GUI.ForceMouseOn(null); if (isEndlessRunner) { @@ -263,7 +263,7 @@ namespace Barotrauma.CharacterEditor else { #if !DEBUG - Reset(Character.CharacterList.Where(c => VanillaCharacters.Any(vchar => vchar == c.ConfigPath))); + Reset(Character.CharacterList.Where(c => VanillaCharacters.Any(vchar => vchar == c.Prefab.ContentFile))); #endif } GameMain.Instance.ResolutionChanged -= OnResolutionChanged; @@ -277,7 +277,7 @@ namespace Barotrauma.CharacterEditor CreateGUI(); } - public static string GetCharacterEditorTranslation(string tag) + public static LocalizedString GetCharacterEditorTranslation(string tag) { return TextManager.Get(screenTextTag + tag); } @@ -815,7 +815,7 @@ namespace Barotrauma.CharacterEditor { if (!limb.Hide) { - limb.body.DebugDraw(spriteBatch, GUI.Style.Green, forceColor: true); + limb.body.DebugDraw(spriteBatch, GUIStyle.Green, forceColor: true); } } } @@ -861,7 +861,7 @@ namespace Barotrauma.CharacterEditor var mouthPos = character.AnimController.GetMouthPosition(); if (mouthPos.HasValue) { - ShapeExtensions.DrawPoint(spriteBatch, SimToScreen(mouthPos.Value), GUI.Style.Red, size: 8); + ShapeExtensions.DrawPoint(spriteBatch, SimToScreen(mouthPos.Value), GUIStyle.Red, size: 8); } } if (showSpritesheet) @@ -877,11 +877,11 @@ namespace Barotrauma.CharacterEditor var textPos = new Vector2(GameMain.GraphicsWidth / 2 - 240, GameMain.GraphicsHeight / 4); if (jointCreationMode == JointCreationMode.Select) { - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectAnchor1Pos"), Color.Yellow, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectAnchor1Pos"), Color.Yellow, font: GUIStyle.LargeFont); } else { - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectLimbToConnect"), Color.Yellow, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectLimbToConnect"), Color.Yellow, font: GUIStyle.LargeFont); } if (jointStartLimb != null && jointStartLimb.ActiveSprite != null) { @@ -890,8 +890,8 @@ namespace Barotrauma.CharacterEditor } if (jointEndLimb != null && jointEndLimb.ActiveSprite != null) { - GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointEndLimb), GUI.Style.Green, thickness: 3); - GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointEndLimb), GUI.Style.Green, thickness: 3); + GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointEndLimb), GUIStyle.Green, thickness: 3); + GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointEndLimb), GUIStyle.Green, thickness: 3); } if (spriteSheetRect.Contains(PlayerInput.MousePosition)) { @@ -901,7 +901,7 @@ namespace Barotrauma.CharacterEditor var offset = anchor1Pos ?? Vector2.Zero; offset = -offset; startPos += offset; - GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUI.Style.Green, width: 3); + GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3); } } else @@ -911,37 +911,37 @@ namespace Barotrauma.CharacterEditor // TODO: there's something wrong here var offset = anchor1Pos.HasValue ? Vector2.Transform(ConvertUnits.ToSimUnits(anchor1Pos.Value), Matrix.CreateRotationZ(jointStartLimb.Rotation)) : Vector2.Zero; var startPos = SimToScreen(jointStartLimb.SimPosition + offset); - GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUI.Style.Green, width: 3); + GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3); } } } if (isDrawingLimb) { var textPos = new Vector2(GameMain.GraphicsWidth / 2 - 200, GameMain.GraphicsHeight / 4); - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("DrawLimbOnSpritesheet"), Color.Yellow, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("DrawLimbOnSpritesheet"), Color.Yellow, font: GUIStyle.LargeFont); } if (isEndlessRunner) { Structure wall = CurrentWall.walls.FirstOrDefault(); Vector2 indicatorPos = wall == null ? originalWall.walls.First().DrawPosition : wall.DrawPosition; - GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUI.SubmarineIcon, Color.White); + GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.White); } GUI.Draw(Cam, spriteBatch); if (isFrozen) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 40, 200), GetCharacterEditorTranslation("Frozen"), Color.Blue, Color.White * 0.5f, 10, GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 40, 200), GetCharacterEditorTranslation("Frozen"), Color.Blue, Color.White * 0.5f, 10, GUIStyle.LargeFont); } if (animTestPoseToggle.Selected) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 300), GetCharacterEditorTranslation("AnimationTestPoseEnabled"), Color.White, Color.Black * 0.5f, 10, GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 300), GetCharacterEditorTranslation("AnimationTestPoseEnabled"), Color.White, Color.Black * 0.5f, 10, GUIStyle.LargeFont); } if (selectedJoints.Count == 1) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedJoints.First().Params.Name}", Color.White, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedJoints.First().Params.Name}", Color.White, font: GUIStyle.LargeFont); } if (selectedLimbs.Count == 1) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedLimbs.First().Params.Name}", Color.White, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedLimbs.First().Params.Name}", Color.White, font: GUIStyle.LargeFont); } if (showSpritesheet) { @@ -958,23 +958,23 @@ namespace Barotrauma.CharacterEditor { var topLeft = spriteSheetControls.RectTransform.TopLeft; bool useSpritesheetOrientation = float.IsNaN(lastLimb.Params.SpriteOrientation); - GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation") + ":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation") + ":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUIStyle.Font); float orientation = useSpritesheetOrientation ? RagdollParams.SpritesheetOrientation : lastLimb.Params.SpriteOrientation; DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), orientation, string.Empty, useSpritesheetOrientation ? Color.White : Color.Yellow, angle => { - TryUpdateSubParam(lastLimb.Params, "spriteorientation", angle); - selectedLimbs.ForEach(l => TryUpdateSubParam(l.Params, "spriteorientation", angle)); + TryUpdateSubParam(lastLimb.Params, "spriteorientation".ToIdentifier(), angle); + selectedLimbs.ForEach(l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), angle)); if (limbPairEditing) { - UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation", angle)); + UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), angle)); } }, circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } else { var topLeft = spriteSheetControls.RectTransform.TopLeft; - GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUIStyle.Font); DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } @@ -990,21 +990,21 @@ namespace Barotrauma.CharacterEditor GUI.DrawLine(spriteBatch, limbDrawPos + Vector2.UnitX * 5.0f, limbDrawPos - Vector2.UnitX * 5.0f, Color.White); } - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 0), $"Cursor World Pos: {character.CursorWorldPosition}", Color.White, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"Cursor Pos: {character.CursorPosition}", Color.White, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 40), $"Cursor Screen Pos: {PlayerInput.MousePosition}", Color.White, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 0), $"Cursor World Pos: {character.CursorWorldPosition}", Color.White, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"Cursor Pos: {character.CursorPosition}", Color.White, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 40), $"Cursor Screen Pos: {PlayerInput.MousePosition}", Color.White, font: GUIStyle.SmallFont); // Collider var collider = character.AnimController.Collider; var colliderDrawPos = SimToScreen(collider.SimPosition); Vector2 forward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); var endPos = SimToScreen(collider.SimPosition + forward * collider.radius); - GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUI.Style.Green); + GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUIStyle.Green); GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + forward * 0.25f), Color.Blue); Vector2 left = forward.Left(); - GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + left * 0.25f), GUI.Style.Red); - ShapeExtensions.DrawCircle(spriteBatch, colliderDrawPos, (endPos - colliderDrawPos).Length(), 40, GUI.Style.Green); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 300, 0), $"Collider rotation: {MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(collider.Rotation))}", Color.White, font: GUI.SmallFont); + GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + left * 0.25f), GUIStyle.Red); + ShapeExtensions.DrawCircle(spriteBatch, colliderDrawPos, (endPos - colliderDrawPos).Length(), 40, GUIStyle.Green); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 300, 0), $"Collider rotation: {MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(collider.Rotation))}", Color.White, font: GUIStyle.SmallFont); } spriteBatch.End(); } @@ -1174,7 +1174,7 @@ namespace Barotrauma.CharacterEditor new XAttribute("height", limb.Params.Height), new XElement("sprite", new XAttribute("texture", spriteParams.Texture), - new XAttribute("sourcerect", $"{rect.X}, {rect.Y}, {rect.Size.X}, {rect.Size.Y}"))); + new XAttribute("sourcerect", $"{rect.X}, {rect.Y}, {rect.Size.X}, {rect.Size.Y}"))).FromPackage(character.Prefab.ContentPackage); CreateLimb(newLimbElement); } @@ -1186,13 +1186,13 @@ namespace Barotrauma.CharacterEditor new XAttribute("height", sourceRect.Height * RagdollParams.TextureScale), new XElement("sprite", new XAttribute("texture", RagdollParams.Limbs.First().GetSprite().Texture), - new XAttribute("sourcerect", $"{sourceRect.X}, {sourceRect.Y}, {sourceRect.Width}, {sourceRect.Height}"))); + new XAttribute("sourcerect", $"{sourceRect.X}, {sourceRect.Y}, {sourceRect.Width}, {sourceRect.Height}"))).FromPackage(character.Prefab.ContentPackage); CreateLimb(newLimbElement); lockSpriteOriginToggle.Selected = false; recalculateColliderToggle.Selected = true; } - private void CreateLimb(XElement newElement) + private void CreateLimb(ContentXElement newElement) { var lastElement = RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastElement != null) @@ -1232,7 +1232,7 @@ namespace Barotrauma.CharacterEditor new XAttribute("limb2", toLimb), new XAttribute("limb1anchor", $"{a1.X.Format(2)}, {a1.Y.Format(2)}"), new XAttribute("limb2anchor", $"{a2.X.Format(2)}, {a2.Y.Format(2)}") - ); + ).FromPackage(character.Prefab.ContentPackage); var lastJointElement = RagdollParams.MainElement.GetChildElements("joint").LastOrDefault() ?? RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastJointElement == null) { @@ -1441,65 +1441,65 @@ namespace Barotrauma.CharacterEditor #region Character spawning private int characterIndex = -1; - private string currentCharacterConfig; - private string selectedJob = null; + private Identifier currentCharacterIdentifier; + private Identifier selectedJob = Identifier.Empty; - private List allFiles; - private List AllFiles + private List allSpecies; + private List AllSpecies { get { - if (allFiles == null) + if (allSpecies == null) { #if DEBUG - allFiles = CharacterPrefab.ConfigFilePaths.OrderBy(p => p).ToList(); + allSpecies = CharacterPrefab.Prefabs.Keys.OrderBy(p => p).ToList(); #else - allFiles = CharacterPrefab.ConfigFilePaths.Where(p => !p.Contains("variant", StringComparison.OrdinalIgnoreCase)).OrderBy(p => p).ToList(); + allSpecies = CharacterPrefab.Prefabs.Keys.Where(p => !p.Contains("variant")).OrderBy(p => p).ToList(); #endif - allFiles.ForEach(f => DebugConsole.NewMessage(f, Color.White)); + allSpecies.ForEach(f => DebugConsole.NewMessage(f.Value, Color.White)); } - return allFiles; + return allSpecies; } } - private List vanillaCharacters; - private List VanillaCharacters + private List vanillaCharacters; + private List VanillaCharacters { get { if (vanillaCharacters == null) { - vanillaCharacters = GameMain.VanillaContent?.GetFilesOfType(ContentType.Character).ToList(); + vanillaCharacters = GameMain.VanillaContent.GetFiles().ToList(); } return vanillaCharacters; } } - private string GetNextConfigFile() + private Identifier GetNextCharacterIdentifier() { GetCurrentCharacterIndex(); IncreaseIndex(); - currentCharacterConfig = AllFiles[characterIndex]; - return currentCharacterConfig; + currentCharacterIdentifier = AllSpecies[characterIndex]; + return currentCharacterIdentifier; } - private string GetPreviousConfigFile() + private Identifier GetPreviousCharacterIdentifier() { GetCurrentCharacterIndex(); ReduceIndex(); - currentCharacterConfig = AllFiles[characterIndex]; - return currentCharacterConfig; + currentCharacterIdentifier = AllSpecies[characterIndex]; + return currentCharacterIdentifier; } private void GetCurrentCharacterIndex() { - characterIndex = AllFiles.IndexOf(CharacterPrefab.FindBySpeciesName(character.SpeciesName).FilePath); + characterIndex = AllSpecies.IndexOf(character.SpeciesName); } private void IncreaseIndex() { characterIndex++; - if (characterIndex > AllFiles.Count - 1) + if (characterIndex > AllSpecies.Count - 1) { characterIndex = 0; } @@ -1510,13 +1510,13 @@ namespace Barotrauma.CharacterEditor characterIndex--; if (characterIndex < 0) { - characterIndex = AllFiles.Count - 1; + characterIndex = AllSpecies.Count - 1; } } - private Character SpawnCharacter(string configFile, RagdollParams ragdoll = null) + private Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) { - DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", configFile.ToString()), Color.HotPink); + DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", speciesName.ToString()), Color.HotPink); OnPreSpawn(); bool dontFollowCursor = true; if (character != null) @@ -1530,10 +1530,10 @@ namespace Barotrauma.CharacterEditor } character = null; } - if (configFile == CharacterPrefab.HumanConfigFile && selectedJob != null) + if (speciesName == CharacterPrefab.HumanSpeciesName && !selectedJob.IsEmpty) { - var characterInfo = new CharacterInfo(configFile, jobPrefab: JobPrefab.Get(selectedJob)); - character = Character.Create(configFile, spawnPosition, ToolBox.RandomSeed(8), characterInfo, hasAi: false, ragdoll: ragdoll); + var characterInfo = new CharacterInfo(speciesName, jobOrJobPrefab: JobPrefab.Prefabs[selectedJob.Value]); + character = Character.Create(speciesName, spawnPosition, ToolBox.RandomSeed(8), characterInfo, hasAi: false, ragdoll: ragdoll); character.GiveJobItems(); HideWearables(); if (displayWearables) @@ -1544,8 +1544,8 @@ namespace Barotrauma.CharacterEditor } else { - character = Character.Create(configFile, spawnPosition, ToolBox.RandomSeed(8), hasAi: false, ragdoll: ragdoll); - selectedJob = null; + character = Character.Create(speciesName, spawnPosition, ToolBox.RandomSeed(8), hasAi: false, ragdoll: ragdoll); + selectedJob = Identifier.Empty; } if (character != null) { @@ -1553,14 +1553,14 @@ namespace Barotrauma.CharacterEditor } if (character == null) { - if (currentCharacterConfig == configFile) + if (currentCharacterIdentifier == speciesName) { return null; } else { // Respawn the current character; - SpawnCharacter(currentCharacterConfig); + SpawnCharacter(currentCharacterIdentifier); } } OnPostSpawn(); @@ -1584,7 +1584,7 @@ namespace Barotrauma.CharacterEditor private void OnPostSpawn() { - currentCharacterConfig = character.ConfigPath; + currentCharacterIdentifier = character.SpeciesName; GetCurrentCharacterIndex(); character.Submarine = Submarine.MainSub; character.AnimController.forceStanding = character.AnimController.CanWalk; @@ -1662,16 +1662,16 @@ namespace Barotrauma.CharacterEditor Cam.Position = character.WorldPosition; } - public bool CreateCharacter(string name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable animations = null) + public bool CreateCharacter(Identifier name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable animations = null) { var vanilla = GameMain.VanillaContent; if (contentPackage == null) { #if DEBUG - contentPackage = GameMain.Config.AllEnabledPackages.LastOrDefault(); + contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(); #else - contentPackage = GameMain.Config.AllEnabledPackages.LastOrDefault(cp => cp != vanilla); + contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(cp => cp != vanilla); #endif } if (contentPackage == null) @@ -1683,24 +1683,24 @@ namespace Barotrauma.CharacterEditor #if !DEBUG if (vanilla != null && contentPackage == vanilla) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } #endif // Content package - if (!GameMain.Config.AllEnabledPackages.Contains(contentPackage)) + if (contentPackage is RegularPackage regular && !ContentPackageManager.EnabledPackages.Regular.Contains(regular)) { - GameMain.Config.EnableRegularPackage(contentPackage); + ContentPackageManager.EnabledPackages.EnableRegular(regular); } - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); // Config file string configFilePath = Path.Combine(mainFolder, $"{name}.xml").Replace(@"\", @"/"); - var duplicate = CharacterPrefab.ConfigFiles.FirstOrDefault(f => (f.Root.IsOverride() ? f.Root.FirstElement() : f.Root).GetAttributeString("speciesname", string.Empty).Equals(name, StringComparison.OrdinalIgnoreCase)); + var duplicate = CharacterPrefab.ConfigElements.FirstOrDefault(e => e.GetAttributeIdentifier("speciesname", Identifier.Empty) == name); XElement overrideElement = null; if (duplicate != null) { - allFiles = null; + allSpecies = null; if (!File.Exists(configFilePath)) { // If the file exists, we just want to overwrite it. @@ -1760,20 +1760,22 @@ namespace Barotrauma.CharacterEditor config = overrideElement; } XDocument doc = new XDocument(config); - if (!Directory.Exists(mainFolder)) - { - Directory.CreateDirectory(mainFolder); - } + + ContentPath configFileContentPath = ContentPath.FromRaw(contentPackage, configFilePath); + Directory.CreateDirectory(Path.GetDirectoryName(configFileContentPath.Value)); #if DEBUG - doc.Save(configFilePath); + doc.Save(configFileContentPath.Value); #else - doc.SaveSafe(configFilePath); + doc.SaveSafe(configFileContentPath.Value); #endif // Add to the selected content package - contentPackage.AddFile(configFilePath, ContentType.Character); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - contentPackage.Save(contentPackage.Path); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + var modProject = new ModProject(contentPackage); + var newFile = ModProject.File.FromPath(configFilePath); + modProject.AddFile(newFile); + + modProject.Save(contentPackage.Path); + contentPackage = ContentPackageManager.ReloadContentPackage(contentPackage); + DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); // Ragdoll @@ -1828,11 +1830,11 @@ namespace Barotrauma.CharacterEditor AnimationParams.Create(fullPath, name, animType, type); } } - if (!AllFiles.Contains(configFilePath)) + if (!AllSpecies.Contains(name)) { - AllFiles.Add(configFilePath); + AllSpecies.Add(name); } - SpawnCharacter(configFilePath, ragdollParams); + SpawnCharacter(name, ragdollParams); limbPairEditing = false; limbsToggle.Selected = true; recalculateColliderToggle.Selected = true; @@ -1976,7 +1978,7 @@ namespace Barotrauma.CharacterEditor AbsoluteSpacing = 2, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("MinorModesTitle"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("MinorModesTitle"), font: GUIStyle.LargeFont); paramsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowParameters")) { Selected = showParamsEditor }; paramsToggle.OnSelected = box => { @@ -2034,7 +2036,7 @@ namespace Barotrauma.CharacterEditor AbsoluteSpacing = 2, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ModesPanel"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ModesPanel"), font: GUIStyle.LargeFont); characterInfoToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditCharacter")) { Selected = editCharacterInfo }; ragdollToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditRagdoll")) { Selected = editRagdoll }; limbsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditLimbs")) { Selected = editLimbs }; @@ -2126,11 +2128,11 @@ namespace Barotrauma.CharacterEditor { if (value) { - toggle.Box.Flash(GUI.Style.Green, useRectangleFlash: true); + toggle.Box.Flash(GUIStyle.Green, useRectangleFlash: true); } else { - toggle.Box.Flash(GUI.Style.Red, useRectangleFlash: true); + toggle.Box.Flash(GUIStyle.Red, useRectangleFlash: true); } } toggle.Selected = value; @@ -2177,7 +2179,7 @@ namespace Barotrauma.CharacterEditor AbsoluteSpacing = 2, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("OptionsPanel"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("OptionsPanel"), font: GUIStyle.LargeFont); freezeToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("Freeze")) { Selected = isFrozen, @@ -2283,24 +2285,24 @@ namespace Barotrauma.CharacterEditor MaxSize = new Point(100, 50) }, style: null, color: Color.Black * 0.6f); var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], - font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = 0; numberInput.MaxValueInt = 255; - numberInput.Font = GUI.SmallFont; + numberInput.Font = GUIStyle.SmallFont; switch (i) { case 0: - colorLabel.TextColor = GUI.Style.Red; + colorLabel.TextColor = GUIStyle.Red; numberInput.IntValue = backgroundColor.R; numberInput.OnValueChanged += (numInput) => backgroundColor.R = (byte)numInput.IntValue; break; case 1: - colorLabel.TextColor = GUI.Style.Green; + colorLabel.TextColor = GUIStyle.Green; numberInput.IntValue = backgroundColor.G; numberInput.OnValueChanged += (numInput) => backgroundColor.G = (byte)numInput.IntValue; break; @@ -2403,10 +2405,10 @@ namespace Barotrauma.CharacterEditor } foreach (var limb in limbs) { - TryUpdateSubParam(limb.Params, "spriteorientation", float.NaN); + TryUpdateSubParam(limb.Params, "spriteorientation".ToIdentifier(), float.NaN); if (limbPairEditing) { - UpdateOtherLimbs(limb, l => TryUpdateSubParam(l.Params, "spriteorientation", float.NaN)); + UpdateOtherLimbs(limb, l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), float.NaN)); } } return true; @@ -2475,11 +2477,11 @@ namespace Barotrauma.CharacterEditor { ToolTip = GetCharacterEditorTranslation("CopyJointSettingsTooltip"), Selected = copyJointSettings, - TextColor = copyJointSettings ? GUI.Style.Red : Color.White, + TextColor = copyJointSettings ? GUIStyle.Red : Color.White, OnSelected = (GUITickBox box) => { copyJointSettings = box.Selected; - box.TextColor = copyJointSettings ? GUI.Style.Red : Color.White; + box.TextColor = copyJointSettings ? GUIStyle.Red : Color.White; return true; } }; @@ -2685,7 +2687,7 @@ namespace Barotrauma.CharacterEditor }; // Character selection - var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUI.LargeFont); + var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.2f, 0.7f), characterLabel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton") { OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; } @@ -2696,25 +2698,25 @@ namespace Barotrauma.CharacterEditor RelativeOffset = new Vector2(0, 0.2f) }, elementCount: 8, style: null); characterDropDown.ListBox.Color = new Color(characterDropDown.ListBox.Color.R, characterDropDown.ListBox.Color.G, characterDropDown.ListBox.Color.B, byte.MaxValue); - foreach (var file in AllFiles) + foreach (var file in AllSpecies) { - characterDropDown.AddItem(Path.GetFileNameWithoutExtension(file).CapitaliseFirstInvariant(), file); + characterDropDown.AddItem(file.Value.CapitaliseFirstInvariant(), file); } - characterDropDown.SelectItem(currentCharacterConfig); + characterDropDown.SelectItem(currentCharacterIdentifier); characterDropDown.OnSelected = (component, data) => { - string configFile = (string)data; + Identifier characterIdentifier = (Identifier)data; try { - SpawnCharacter(configFile); + SpawnCharacter(characterIdentifier); } catch (Exception e) { - HandleSpawnException(configFile, e); + HandleSpawnException(characterIdentifier, e); } return true; }; - if (currentCharacterConfig == CharacterPrefab.HumanConfigFile) + if (currentCharacterIdentifier == CharacterPrefab.HumanSpeciesName) { var jobDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.15f), content.RectTransform) { @@ -2726,11 +2728,11 @@ namespace Barotrauma.CharacterEditor jobDropDown.SelectItem(selectedJob); jobDropDown.OnSelected = (component, data) => { - string newJob = data is string jobIdentifier ? jobIdentifier : null; + Identifier newJob = data is Identifier jobIdentifier ? jobIdentifier : Identifier.Empty; if (newJob != selectedJob) { selectedJob = newJob; - SpawnCharacter(currentCharacterConfig); + SpawnCharacter(currentCharacterIdentifier); } return true; }; @@ -2740,14 +2742,14 @@ namespace Barotrauma.CharacterEditor prevCharacterButton.TextBlock.AutoScaleHorizontal = true; prevCharacterButton.OnClicked += (b, obj) => { - string configFile = GetPreviousConfigFile(); + Identifier characterIdentifier = GetPreviousCharacterIdentifier(); try { - SpawnCharacter(configFile); + SpawnCharacter(characterIdentifier); } catch (Exception e) { - HandleSpawnException(configFile, e); + HandleSpawnException(characterIdentifier, e); } return true; }; @@ -2755,14 +2757,14 @@ namespace Barotrauma.CharacterEditor prevCharacterButton.TextBlock.AutoScaleHorizontal = true; nextCharacterButton.OnClicked += (b, obj) => { - string configFile = GetNextConfigFile(); + Identifier characterIdentifier = GetNextCharacterIdentifier(); try { - SpawnCharacter(configFile); + SpawnCharacter(characterIdentifier); } catch (Exception e) { - HandleSpawnException(configFile, e); + HandleSpawnException(characterIdentifier, e); } return true; }; @@ -2770,16 +2772,16 @@ namespace Barotrauma.CharacterEditor characterPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), characterSelectionPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right); characterSelectionPanel.RectTransform.MinSize = new Point(0, (int)(content.RectTransform.Children.Sum(c => c.MinSize.Y) * 1.2f)); - void HandleSpawnException(string configFile, Exception e) + void HandleSpawnException(Identifier characterIdentifier, Exception e) { - if (configFile != CharacterPrefab.HumanConfigFile) + if (characterIdentifier != CharacterPrefab.HumanSpeciesName) { - DebugConsole.ThrowError($"Failed to spawn the character \"{configFile}\".", e); - SpawnCharacter(CharacterPrefab.HumanConfigFile); + DebugConsole.ThrowError($"Failed to spawn the character \"{characterIdentifier}\".", e); + SpawnCharacter(CharacterPrefab.HumanSpeciesName); } else { - throw new Exception($"Failed to spawn the character \"{configFile}\".", innerException: e); + throw new Exception($"Failed to spawn the character \"{characterIdentifier}\".", innerException: e); } } } @@ -2795,18 +2797,18 @@ namespace Barotrauma.CharacterEditor Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("FileEditPanel"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("FileEditPanel"), font: GUIStyle.LargeFont); // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; var saveAllButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), TextManager.Get("editor.saveall")); - saveAllButton.Color = GUI.Style.Green; + saveAllButton.Color = GUIStyle.Green; saveAllButton.OnClicked += (button, userData) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) + if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } #endif @@ -2818,9 +2820,9 @@ namespace Barotrauma.CharacterEditor else { character.Params.Save(); - GUI.AddMessage(GetCharacterEditorTranslation("CharacterSavedTo").Replace("[path]", CharacterParams.FullPath), GUI.Style.Green, font: GUI.Font, lifeTime: 5); + GUI.AddMessage(GetCharacterEditorTranslation("CharacterSavedTo").Replace("[path]", CharacterParams.Path.Value), GUIStyle.Green, font: GUIStyle.Font, lifeTime: 5); character.AnimController.SaveRagdoll(); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.FullPath), GUI.Style.Green, font: GUI.Font, lifeTime: 5); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), GUIStyle.Green, font: GUIStyle.Font, lifeTime: 5); AnimParams.ForEach(p => p.Save()); } return true; @@ -2833,7 +2835,7 @@ namespace Barotrauma.CharacterEditor var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll")); saveRagdollButton.OnClicked += (button, userData) => { - var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); + var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var inputField = new GUITextBox(new RectTransform(new Point(box.Content.Rect.Width, (int)(30 * GUI.yScale)), box.Content.RectTransform, Anchor.Center), RagdollParams.Name.RemoveWhitespace()); box.Buttons[0].OnClicked += (b, d) => { @@ -2843,15 +2845,15 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) + if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); return false; } #endif character.AnimController.SaveRagdoll(inputField.Text); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.FullPath), Color.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), Color.Green, font: GUIStyle.Font); box.Close(); return true; }; @@ -2860,7 +2862,7 @@ namespace Barotrauma.CharacterEditor var loadRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadRagdoll")); loadRagdollButton.OnClicked += (button, userData) => { - var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); + var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)); var deleteButton = loadBox.Buttons[2]; @@ -2873,7 +2875,7 @@ namespace Barotrauma.CharacterEditor foreach (var path in filePaths) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUI.Font, listBox.Rect.Width - 80)) + ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80)) { UserData = path, ToolTip = path @@ -2905,14 +2907,14 @@ namespace Barotrauma.CharacterEditor } var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), - TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", selectedFile), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked += (b, d) => { try { File.Delete(selectedFile); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollDeletedFrom").Replace("[file]", selectedFile), GUI.Style.Red, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollDeletedFrom").Replace("[file]", selectedFile), GUIStyle.Red, font: GUIStyle.Font); } catch (Exception e) { @@ -2936,7 +2938,7 @@ namespace Barotrauma.CharacterEditor string fileName = Path.GetFileNameWithoutExtension(selectedFile); var ragdoll = character.IsHumanoid ? HumanRagdollParams.GetRagdollParams(character.SpeciesName, fileName) as RagdollParams : RagdollParams.GetRagdollParams(character.SpeciesName, fileName); ragdoll.Reset(true); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollLoadedFrom").Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollLoadedFrom").Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); RecreateRagdoll(ragdoll); CreateContextualControls(); loadBox.Close(); @@ -2947,7 +2949,7 @@ namespace Barotrauma.CharacterEditor var saveAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveAnimation")); saveAnimationButton.OnClicked += (button, userData) => { - var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); + var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.1f), box.Content.RectTransform) { MinSize = new Point(350, 30) }, style: null); var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform, Anchor.CenterLeft) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: "); var inputField = new GUITextBox(new RectTransform(new Vector2(0.45f, 1), textArea.RectTransform, Anchor.CenterRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name); @@ -2978,9 +2980,9 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) + if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); return false; } @@ -2988,7 +2990,7 @@ namespace Barotrauma.CharacterEditor var animParams = character.AnimController.GetAnimationParamsFromType(selectedType); if (animParams == null) { return true; } animParams.Save(inputField.Text); - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", animParams.AnimationType.ToString()).Replace("[path]", animParams.FullPath), Color.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", animParams.AnimationType.ToString()).Replace("[path]", animParams.Path.Value), Color.Green, font: GUIStyle.Font); ResetParamsEditor(); box.Close(); return true; @@ -2998,7 +3000,7 @@ namespace Barotrauma.CharacterEditor var loadAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadAnimation")); loadAnimationButton.OnClicked += (button, userData) => { - var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); + var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)); var deleteButton = loadBox.Buttons[2]; @@ -3030,7 +3032,7 @@ namespace Barotrauma.CharacterEditor var filePaths = Directory.GetFiles(CurrentAnimation.Folder); foreach (var path in AnimationParams.FilterFilesByType(filePaths, selectedType)) { - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUI.Font, listBox.Rect.Width - 80)) + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80)) { UserData = path, ToolTip = path @@ -3062,18 +3064,18 @@ namespace Barotrauma.CharacterEditor } var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), - TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", selectedFile), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked += (b, d) => { try { File.Delete(selectedFile); - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeDeleted").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), GUI.Style.Red, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeDeleted").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), GUIStyle.Red, font: GUIStyle.Font); } catch (Exception e) { - DebugConsole.ThrowError(TextManager.Get("DeleteFileError").Replace("[file]", selectedFile), e); + DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", selectedFile), e); } msgBox.Close(); PopulateListBox(); @@ -3135,7 +3137,7 @@ namespace Barotrauma.CharacterEditor break; } } - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); character.AnimController.AllAnimParams.ForEach(a => a.Reset(forceReload: true)); ResetParamsEditor(); loadBox.Close(); @@ -3147,7 +3149,7 @@ namespace Barotrauma.CharacterEditor // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; var resetButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ResetButton")); - resetButton.Color = GUI.Style.Red; + resetButton.Color = GUIStyle.Red; resetButton.OnClicked += (button, userData) => { CharacterParams.Reset(true); @@ -3426,7 +3428,7 @@ namespace Barotrauma.CharacterEditor { CanBeFocused = false }; - new GUIButton(new RectTransform(new Vector2(0.9f), parent.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton", color: GUI.Style.Red) + new GUIButton(new RectTransform(new Vector2(0.9f), parent.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton", color: GUIStyle.Red) { OnClicked = (button, data) => { @@ -3438,7 +3440,7 @@ namespace Barotrauma.CharacterEditor editor.AddCustomContent(parent, 0); } - void CreateAddButtonAtLast(ParamsEditor editor, Action onButtonClicked, string text) + void CreateAddButtonAtLast(ParamsEditor editor, Action onButtonClicked, LocalizedString text) { if (editor == null) { return; } var parentFrame = new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, (int)(50 * GUI.yScale)), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) @@ -3456,7 +3458,7 @@ namespace Barotrauma.CharacterEditor }; } - void CreateAddButton(SerializableEntityEditor editor, Action onButtonClicked, string text) + void CreateAddButton(SerializableEntityEditor editor, Action onButtonClicked, LocalizedString text) { if (editor == null) { return; } var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, (int)(60 * GUI.yScale)), editor.RectTransform), style: null) @@ -3476,10 +3478,12 @@ namespace Barotrauma.CharacterEditor } } - private void TryUpdateAnimParam(string name, object value) => TryUpdateParam(character.AnimController.CurrentAnimationParams, name, value); - private void TryUpdateRagdollParam(string name, object value) => TryUpdateParam(RagdollParams, name, value); + private void TryUpdateAnimParam(string name, object value) => TryUpdateAnimParam(name.ToIdentifier(), value); + private void TryUpdateAnimParam(Identifier name, object value) => TryUpdateParam(character.AnimController.CurrentAnimationParams, name, value); + private void TryUpdateRagdollParam(string name, object value) => TryUpdateRagdollParam(name.ToIdentifier(), value); + private void TryUpdateRagdollParam(Identifier name, object value) => TryUpdateParam(RagdollParams, name, value); - private void TryUpdateParam(EditableParams editableParams, string name, object value) + private void TryUpdateParam(EditableParams editableParams, Identifier name, object value) { if (editableParams.SerializableEntityEditor == null) { @@ -3491,10 +3495,12 @@ namespace Barotrauma.CharacterEditor } } - private void TryUpdateJointParam(LimbJoint joint, string name, object value) => TryUpdateSubParam(joint.Params, name, value); - private void TryUpdateLimbParam(Limb limb, string name, object value) => TryUpdateSubParam(limb.Params, name, value); + private void TryUpdateJointParam(LimbJoint joint, string name, object value) => TryUpdateJointParam(joint, name.ToIdentifier(), value); + private void TryUpdateJointParam(LimbJoint joint, Identifier name, object value) => TryUpdateSubParam(joint.Params, name, value); + private void TryUpdateLimbParam(Limb limb, string name, object value) => TryUpdateLimbParam(limb, name.ToIdentifier(), value); + private void TryUpdateLimbParam(Limb limb, Identifier name, object value) => TryUpdateSubParam(limb.Params, name, value); - private void TryUpdateSubParam(RagdollParams.SubParam ragdollSubParams, string name, object value) + private void TryUpdateSubParam(RagdollParams.SubParam ragdollSubParams, Identifier name, object value) { if (ragdollSubParams.SerializableEntityEditor == null) { @@ -3520,7 +3526,7 @@ namespace Barotrauma.CharacterEditor } else { - DebugConsole.ThrowError(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name)); + DebugConsole.ThrowError(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name.Value)); } } } @@ -3806,7 +3812,7 @@ namespace Barotrauma.CharacterEditor bool ShowCycleWidget() => PlayerInput.KeyDown(Keys.LeftAlt) && (CurrentAnimation is IHumanAnimation || CurrentAnimation is GroundedMovementParams); if (!PlayerInput.KeyDown(Keys.LeftAlt) && (animParams is IHumanAnimation || animParams is GroundedMovementParams)) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 120, 150), GetCharacterEditorTranslation("HoldLeftAltToAdjustCycleSpeed"), Color.White, Color.Black * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 120, 150), GetCharacterEditorTranslation("HoldLeftAltToAdjustCycleSpeed"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font); } // Widgets for all anims --> Vector2 referencePoint = SimToScreen(head != null ? head.SimPosition: collider.SimPosition); @@ -3915,7 +3921,7 @@ namespace Barotrauma.CharacterEditor DrawRadialWidget(spriteBatch, SimToScreen(head.SimPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + head.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); // Head position and leaning - Color color = GUI.Style.Red; + Color color = GUIStyle.Red; if (animParams.IsGroundedAnimation) { if (humanGroundedParams != null && character.AnimController is HumanoidAnimController humanAnimController) @@ -4193,7 +4199,7 @@ namespace Barotrauma.CharacterEditor { if (hand != null || arm != null) { - GetAnimationWidget("HandMoveAmount", GUI.Style.Green, Color.Black, initMethod: w => + GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); float offset = 0.1f; @@ -4214,7 +4220,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); + GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4333,7 +4339,7 @@ namespace Barotrauma.CharacterEditor lengthWidget.Draw(spriteBatch, deltaTime); amplitudeWidget.Draw(spriteBatch, deltaTime); // Arms - GetAnimationWidget("HandMoveAmount", GUI.Style.Green, Color.Black, initMethod: w => + GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); float offset = 0.4f; @@ -4356,7 +4362,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); + GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4367,7 +4373,7 @@ namespace Barotrauma.CharacterEditor if (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot) { GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugRefPos) - Vector2.One * 3, Vector2.One * 6, Color.White, isFilled: true); - GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugTargetPos) - Vector2.One * 3, Vector2.One * 6, GUI.Style.Green, isFilled: true); + GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugTargetPos) - Vector2.One * 3, Vector2.One * 6, GUIStyle.Green, isFilled: true); } } } @@ -4451,7 +4457,7 @@ namespace Barotrauma.CharacterEditor if (!altDown && editJoints && selectedJoints.Any() && jointCreationMode == JointCreationMode.None) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 100), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 100), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font); } foreach (Limb limb in character.AnimController.Limbs) @@ -4462,7 +4468,7 @@ namespace Barotrauma.CharacterEditor { var pullJointWidgetSize = new Vector2(5, 5); Vector2 tformedPullPos = SimToScreen(limb.PullJointWorldAnchorA); - GUI.DrawRectangle(spriteBatch, tformedPullPos - pullJointWidgetSize / 2, pullJointWidgetSize, GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, tformedPullPos - pullJointWidgetSize / 2, pullJointWidgetSize, GUIStyle.Red, true); DrawWidget(spriteBatch, tformedPullPos, WidgetType.Rectangle, 8, Color.Cyan, $"IK ({limb.Name})", () => { if (!selectedLimbs.Contains(limb)) @@ -4540,7 +4546,7 @@ namespace Barotrauma.CharacterEditor //GUI.DrawRectangle(spriteBatch, tformedJointPos - dotSize / 2, dotSize, color, true); //GUI.DrawLine(spriteBatch, tformedJointPos, tformedJointPos + up * 20, Color.White, width: 3); GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow, width: 3); - //GUI.DrawRectangle(spriteBatch, inputRect, GUI.Style.Red); + //GUI.DrawRectangle(spriteBatch, inputRect, GUIStyle.Red); GUI.DrawString(spriteBatch, tformedJointPos + new Vector2(dotSize.X, -dotSize.Y) * 2, $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}", Color.White, Color.Black * 0.5f); if (PlayerInput.PrimaryMouseButtonHeld()) { @@ -4723,10 +4729,10 @@ namespace Barotrauma.CharacterEditor texturePaths = new List(); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.ActiveSprite == null || texturePaths.Contains(limb.ActiveSprite.FilePath)) { continue; } + if (limb.ActiveSprite == null || texturePaths.Contains(limb.ActiveSprite.FilePath.Value)) { continue; } if (limb.ActiveSprite.Texture == null) { continue; } textures.Add(limb.ActiveSprite.Texture); - texturePaths.Add(limb.ActiveSprite.FilePath); + texturePaths.Add(limb.ActiveSprite.FilePath.Value); } } @@ -4806,7 +4812,7 @@ namespace Barotrauma.CharacterEditor { if (isSelected || !onlyShowSourceRectForSelectedLimbs) { - GUI.DrawRectangle(spriteBatch, rect, isSelected ? Color.Yellow : (isMouseOn ? Color.White : GUI.Style.Red)); + GUI.DrawRectangle(spriteBatch, rect, isSelected ? Color.Yellow : (isMouseOn ? Color.White : GUIStyle.Red)); } } if (isSelected) @@ -5130,7 +5136,7 @@ namespace Barotrauma.CharacterEditor private void DrawJointLimitWidgets(SpriteBatch spriteBatch, Limb limb, LimbJoint joint, Vector2 drawPos, bool autoFreeze, bool allowPairEditing, bool holdPosition, float rotationOffset = 0) { bool clockWise = joint.Params.ClockWiseRotation; - Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? GUI.Style.Green * 0.5f : GUI.Style.Red; + Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? GUIStyle.Green * 0.5f : GUIStyle.Red; DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.UpperLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("UpperLimit")}", Color.Cyan, angle => { joint.UpperLimit = MathHelper.ToRadians(angle); @@ -5168,7 +5174,7 @@ namespace Barotrauma.CharacterEditor } DrawAngle(20, angleColor, 4); DrawAngle(40, Color.Cyan); - GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Cyan, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Cyan, font: GUIStyle.SmallFont); }, circleRadius: 40, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition); DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.LowerLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("LowerLimit")}", Color.Yellow, angle => { @@ -5207,7 +5213,7 @@ namespace Barotrauma.CharacterEditor } DrawAngle(20, angleColor, 4); DrawAngle(25, Color.Yellow); - GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Yellow, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Yellow, font: GUIStyle.SmallFont); }, circleRadius: 25, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition); void DrawAngle(float radius, Color color, float thickness = 5) { @@ -5314,7 +5320,7 @@ namespace Barotrauma.CharacterEditor #endregion #region Widgets as methods - private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, string toolTip, Color color, Action onClick, + private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, LocalizedString toolTip, Color color, Action onClick, float circleRadius = 30, int widgetSize = 10, float rotationOffset = 0, bool clockWise = true, bool displayAngle = true, bool? autoFreeze = null, bool wrapAnglePi = false, bool holdPosition = false, int rounding = 1) { var angle = value; @@ -5338,11 +5344,11 @@ namespace Barotrauma.CharacterEditor if (angle >= 360 || angle <= -360) { angle = 0; } if (displayAngle) { - GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUIStyle.SmallFont); } onClick(angle); var zeroPos = drawPos + VectorExtensions.Forward(rotationOffset - MathHelper.PiOver2, circleRadius); - GUI.DrawLine(spriteBatch, drawPos, zeroPos, GUI.Style.Red, width: 3); + GUI.DrawLine(spriteBatch, drawPos, zeroPos, GUIStyle.Red, width: 3); }, autoFreeze, holdPosition, onHovered: () => { if (!PlayerInput.PrimaryMouseButtonHeld()) @@ -5354,7 +5360,7 @@ namespace Barotrauma.CharacterEditor } private enum WidgetType { Rectangle, Circle } - private void DrawWidget(SpriteBatch spriteBatch, Vector2 drawPos, WidgetType widgetType, int size, Color color, string toolTip, Action onPressed, bool? autoFreeze = null, bool holdPosition = false, Action onHovered = null) + private void DrawWidget(SpriteBatch spriteBatch, Vector2 drawPos, WidgetType widgetType, int size, Color color, LocalizedString toolTip, Action onPressed, bool? autoFreeze = null, bool holdPosition = false, Action onHovered = null) { var drawRect = new Rectangle((int)drawPos.X - size / 2, (int)drawPos.Y - size / 2, size, size); var inputRect = drawRect; @@ -5501,7 +5507,7 @@ namespace Barotrauma.CharacterEditor widget.refresh = () => { widget.showTooltip = !selectedJoints.Contains(joint); - widget.color = selectedJoints.Contains(joint) ? Color.Yellow : GUI.Style.Red; + widget.color = selectedJoints.Contains(joint) ? Color.Yellow : GUIStyle.Red; }; widget.refresh(); widget.PreUpdate += dTime => widget.Enabled = editJoints; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 5c41420ac..48336c09b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -11,7 +11,7 @@ namespace Barotrauma.CharacterEditor class Wizard { // Ragdoll data - private string name; + private Identifier name; private bool isHumanoid; private bool canEnterSubmarine = true; private bool canWalk; @@ -39,7 +39,7 @@ namespace Barotrauma.CharacterEditor canEnterSubmarine = ragdoll.CanEnterSubmarine; canWalk = ragdoll.CanWalk; texturePath = ragdoll.Texture; - if (string.IsNullOrEmpty(texturePath) && !name.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(texturePath) && name != CharacterPrefab.HumanSpeciesName) { texturePath = ragdoll.Limbs.FirstOrDefault()?.GetSprite().Texture; } @@ -58,7 +58,7 @@ namespace Barotrauma.CharacterEditor } } - public static string GetCharacterEditorTranslation(string text) => CharacterEditorScreen.GetCharacterEditorTranslation(text); + public static LocalizedString GetCharacterEditorTranslation(string text) => CharacterEditorScreen.GetCharacterEditorTranslation(text); public void Reset() { @@ -97,11 +97,11 @@ namespace Barotrauma.CharacterEditor public void CreateCharacter(XElement ragdollElement, XElement characterElement = null, IEnumerable animations = null) { - if (CharacterPrefab.Find(p => p.Identifier.Equals(name, StringComparison.OrdinalIgnoreCase)) != null) + if (CharacterPrefab.Find(p => p.Identifier == name) != null) { - bool isSamePackage = contentPackage.GetFilesOfType(ContentType.Character).Any(c => Path.GetFileNameWithoutExtension(c).Equals(name, StringComparison.OrdinalIgnoreCase)); - string verificationText = isSamePackage ? GetCharacterEditorTranslation("existingcharacterfoundreplaceverification") : GetCharacterEditorTranslation("existingcharacterfoundoverrideverification"); - var msgBox = new GUIMessageBox("", verificationText, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + 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" }; @@ -110,7 +110,7 @@ namespace Barotrauma.CharacterEditor msgBox.Close(); if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) { - GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name), GUI.Style.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name.Value), GUIStyle.Green, font: GUIStyle.Font); } Wizard.Instance.SelectTab(Tab.None); return true; @@ -126,7 +126,7 @@ namespace Barotrauma.CharacterEditor { if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) { - GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name), GUI.Style.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name.Value), GUIStyle.Green, font: GUIStyle.Font); } Wizard.Instance.SelectTab(Tab.None); } @@ -141,8 +141,8 @@ namespace Barotrauma.CharacterEditor protected override GUIMessageBox Create() { - var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new string[] { TextManager.Get("Cancel"), IsCopy ? TextManager.Get("Create") : TextManager.Get("Next") }, new Vector2(0.65f, 0.9f)); - box.Header.Font = GUI.LargeFont; + 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; @@ -161,7 +161,7 @@ namespace Barotrauma.CharacterEditor void UpdatePaths() { string pathBase = ContentPackage == GameMain.VanillaContent ? $"Content/Characters/{Name}/{Name}" - : $"Mods/{(ContentPackage != null ? ContentPackage.Name + "/" : string.Empty)}Characters/{Name}/{Name}"; + : $"{ContentPath.ModDirStr}/Characters/{Name}/{Name}"; XMLPath = $"{pathBase}.xml"; xmlPathElement.Text = XMLPath; if (updateTexturePath) @@ -178,12 +178,12 @@ namespace Barotrauma.CharacterEditor { 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 ?? GetCharacterEditorTranslation("DefaultName")) { CaretColor = Color.White }; + 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); + Name = ProcessText(nameField.Text).ToIdentifier(); nameField.OnTextChanged += (tb, text) => { - Name = ProcessText(text); + Name = ProcessText(text).ToIdentifier(); UpdatePaths(); return true; }; @@ -255,7 +255,7 @@ namespace Barotrauma.CharacterEditor TexturePath = text; return true; }; - string title = GetCharacterEditorTranslation("SelectTexture"); + 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) => @@ -305,12 +305,12 @@ namespace Barotrauma.CharacterEditor 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 cp in GameMain.Config.AllEnabledPackages) + foreach (ContentPackage contentPackage in ContentPackageManager.EnabledPackages.All) { #if !DEBUG - if (cp == GameMain.VanillaContent) { continue; } + if (contentPackage == GameMain.VanillaContent) { continue; } #endif - contentPackageDropDown.AddItem(cp.Name, userData: cp, toolTip: cp.Path); + contentPackageDropDown.AddItem(contentPackage.Name, userData: contentPackage, toolTip: contentPackage.Path); } contentPackageDropDown.OnSelected = (obj, userdata) => { @@ -321,7 +321,7 @@ namespace Barotrauma.CharacterEditor }; contentPackageDropDown.Select(0); var contentPackageNameElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 0.5f), rightContainer.RectTransform, Anchor.BottomLeft), - GetCharacterEditorTranslation("NewContentPackage")) + GetCharacterEditorTranslation("NewContentPackage").Value) { CaretColor = Color.White, }; @@ -334,15 +334,16 @@ namespace Barotrauma.CharacterEditor contentPackageNameElement.Flash(); return false; } - if (ContentPackage.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) + if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) { - new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", fallBackTag: "leveleditorlevelobjnametaken")); + new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken")); return false; } - string modName = ToolBox.RemoveInvalidFileNameChars(contentPackageNameElement.Text); - ContentPackage = ContentPackage.CreatePackage(contentPackageNameElement.Text, Path.Combine("Mods", modName, Steam.SteamManager.MetadataFileName), false); - ContentPackage.AddPackage(ContentPackage); - GameMain.Config.EnableRegularPackage(ContentPackage); + 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 = ""; @@ -389,15 +390,15 @@ namespace Barotrauma.CharacterEditor } if (!File.Exists(TexturePath)) { - GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUI.Style.Red); - texturePathElement.Flash(GUI.Style.Red); + GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red); + texturePathElement.Flash(GUIStyle.Red); return false; } var path = Path.GetFileName(TexturePath); if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { - GUI.AddMessage(TextManager.Get("WrongFileType"), GUI.Style.Red); - texturePathElement.Flash(GUI.Style.Red); + GUI.AddMessage(TextManager.Get("WrongFileType"), GUIStyle.Red); + texturePathElement.Flash(GUIStyle.Red); return false; } if (IsCopy) @@ -427,8 +428,8 @@ namespace Barotrauma.CharacterEditor protected override GUIMessageBox Create() { - var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new string[] { TextManager.Get("Previous"), TextManager.Get("Create") }, new Vector2(0.65f, 1f)); - box.Header.Font = GUI.LargeFont; + 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); @@ -453,7 +454,7 @@ namespace Barotrauma.CharacterEditor Stretch = true, RelativeSpacing = 0.02f }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbEditLayout.RectTransform), GetCharacterEditorTranslation("Limbs"), font: GUI.SubHeadingFont); + 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)); var removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") { @@ -494,10 +495,10 @@ namespace Barotrauma.CharacterEditor 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: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + 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), GUINumberInput.NumberType.Int) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; switch (i) { @@ -623,7 +624,7 @@ namespace Barotrauma.CharacterEditor // 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: GUI.SubHeadingFont); + 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) @@ -658,12 +659,12 @@ namespace Barotrauma.CharacterEditor { if (htmlBox == null) { - htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new string[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); - htmlBox.Header.Font = GUI.LargeFont; + 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")); - string title = GetCharacterEditorTranslation("SelectFile"); + 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) => @@ -732,14 +733,14 @@ namespace Barotrauma.CharacterEditor LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("head", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (main == null) { - GUI.AddMessage(GetCharacterEditorTranslation("MissingTorsoOrHead"), GUI.Style.Red); + 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()), GUI.Style.Red); + GUI.AddMessage(GetCharacterEditorTranslation("MissingLimbType").Replace("[limbtype]", missingType.FormatCamelCaseWithSpaces()), GUIStyle.Red); return false; } } @@ -829,11 +830,11 @@ namespace Barotrauma.CharacterEditor 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: GUI.SubHeadingFont); + 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: GUI.Font); - var sourceRectField = GUI.CreateRectangleField(sourceRect ?? new Rectangle(0, 100 * LimbGUIElements.Count, 100, 100), elementSize, GetCharacterEditorTranslation("SourceRectangle"), group.RectTransform, font: GUI.Font); + 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), GUINumberInput.NumberType.Int) { @@ -869,7 +870,7 @@ namespace Barotrauma.CharacterEditor 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: GUI.SubHeadingFont); + 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) @@ -898,8 +899,8 @@ namespace Barotrauma.CharacterEditor MaxValueInt = byte.MaxValue, IntValue = id2 }; - GUI.CreateVector2Field(anchor1 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "1"), group.RectTransform, font: GUI.Font, decimalsToDisplay: 2); - GUI.CreateVector2Field(anchor2 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "2"), group.RectTransform, font: GUI.Font, decimalsToDisplay: 2); + 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); @@ -917,7 +918,7 @@ namespace Barotrauma.CharacterEditor public CharacterParams SourceCharacter => Instance.SourceCharacter; public RagdollParams SourceRagdoll => Instance.SourceRagdoll; - public string Name + public Identifier Name { get => Instance.name; set => Instance.name = value; @@ -1005,7 +1006,7 @@ namespace Barotrauma.CharacterEditor { var limbGUIElement = LimbGUIElements[i]; var allChildren = limbGUIElement.GetAllChildren(); - GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; + 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; @@ -1056,7 +1057,7 @@ namespace Barotrauma.CharacterEditor { var jointGUIElement = JointGUIElements[i]; var allChildren = jointGUIElement.GetAllChildren(); - GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; + 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index 18ae1af3d..e191d3e54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs @@ -8,7 +8,7 @@ namespace Barotrauma { private GUIListBox listBox; - private XElement configElement; + private ContentXElement configElement; private float scrollSpeed; @@ -48,7 +48,7 @@ namespace Barotrauma var doc = XMLExtensions.TryLoadXml(configFile); if (doc == null) { return; } - configElement = doc.Root; + configElement = doc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); Load(); } @@ -63,7 +63,7 @@ namespace Barotrauma Spacing = spacing }; - foreach (XElement subElement in configElement.Elements()) + foreach (var subElement in configElement.Elements()) { FromXML(subElement, listBox.Content.RectTransform); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs index dab80c527..6e102be08 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs @@ -93,7 +93,7 @@ namespace Barotrauma public bool EditorMode; - private string editModeText = ""; + private LocalizedString editModeText = ""; private Vector2 textSize = Vector2.Zero; public void Save(XElement element) @@ -117,7 +117,7 @@ namespace Barotrauma { Clear(alsoPending: true); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { EditorImageContainer? tempImage = EditorImageContainer.Load(subElement); if (tempImage != null) @@ -130,7 +130,7 @@ namespace Barotrauma public void OnEditorSelected() { editModeText = TextManager.Get("SubEditor.ImageEditingMode"); - textSize = GUI.LargeFont.MeasureString(editModeText); + textSize = GUIStyle.LargeFont.MeasureString(editModeText); TryLoadPendingImages(); } @@ -267,7 +267,7 @@ namespace Barotrauma pos.Y = -pos.Y; Images.Add(new EditorImage(file, pos) { DrawTarget = EditorImage.DrawTargetType.World }); UpdateImageCategories(); - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); }; FileSelection.ClearFileTypeFilters(); @@ -287,7 +287,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2f - (textSize.X / 2f), GameMain.GraphicsHeight / 10f - (textSize.Y / 2f)); - GUI.DrawString(spriteBatch, textPos, editModeText, GUI.Style.Yellow, Color.Black * 0.4f, 8, GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, editModeText, GUIStyle.Yellow, Color.Black * 0.4f, 8, GUIStyle.LargeFont); spriteBatch.End(); } @@ -352,7 +352,7 @@ namespace Barotrauma public EditorImage(string path, Vector2 pos) { - Image = Sprite.LoadTexture(path, out Sprite _, compress: false); + Image = Sprite.LoadTexture(path, compress: false); ImagePath = path; Position = pos; UpdateRectangle(); @@ -440,7 +440,7 @@ namespace Barotrauma { widget.MouseDown += () => { - widget.color = GUI.Style.Green; + widget.color = GUIStyle.Green; prevAngle = Rotation; disableMove = true; }; @@ -493,7 +493,7 @@ namespace Barotrauma }); currentWidget.Draw(spriteBatch, (float) Timing.Step); - GUI.DrawLine(spriteBatch, Position, currentWidget.DrawPos, GUI.Style.Green, width: width); + GUI.DrawLine(spriteBatch, Position, currentWidget.DrawPos, GUIStyle.Green, width: width); } private float GetRotationAngle(Vector2 drawPosition) @@ -561,7 +561,7 @@ namespace Barotrauma width = (int) (width / cam.Zoom); } - GUI.DrawRectangle(spriteBatch, bounds, Selected ? GUI.Style.Red : GUI.Style.Green, thickness: width); + GUI.DrawRectangle(spriteBatch, bounds, Selected ? GUIStyle.Red : GUIStyle.Green, thickness: width); if (Selected) { DrawWidgets(spriteBatch); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 6da198254..9b68e1dbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -4,7 +4,8 @@ namespace Barotrauma { class EditorScreen : Screen { - public static Color BackgroundColor = GameSettings.SubEditorBackgroundColor; + public static Color BackgroundColor = GameSettings.CurrentConfig.SubEditorBackground; + public override bool IsEditor => true; public void CreateBackgroundColorPicker() { @@ -17,7 +18,7 @@ namespace Barotrauma for (int i = 0; i < 3; i++) { var colorContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1), rgbLayout.RectTransform), isHorizontal: true) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.ColorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); layoutParents[i] = colorContainer; } @@ -33,7 +34,9 @@ namespace Barotrauma { var color = new Color(rInput.IntValue, gInput.IntValue, bInput.IntValue); BackgroundColor = color; - GameSettings.SubEditorBackgroundColor = color; + var config = GameSettings.CurrentConfig; + config.SubEditorBackground = color; + GameSettings.SetCurrentConfig(config); }; // Reset button @@ -49,7 +52,7 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked = (button, o) => { msgBox.Close(); - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); return true; }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs index 5e92d8477..3b8f0f22b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -63,10 +63,10 @@ namespace Barotrauma connectionElement.Add(new XAttribute("optiontext", connection.OptionText)); } - if (!string.IsNullOrWhiteSpace(connection.OverrideValue?.ToString())) + if (connection.OverrideValue is { } overrideValue && !string.IsNullOrWhiteSpace(connection.OverrideValue?.ToString())) { - connectionElement.Add(new XAttribute("overridevalue", connection.OverrideValue?.ToString())); - connectionElement.Add(new XAttribute("valuetype", connection.OverrideValue?.GetType().ToString())); + connectionElement.Add(new XAttribute("overridevalue", overrideValue.ToString() ?? string.Empty)); + connectionElement.Add(new XAttribute("valuetype", overrideValue.GetType().ToString())); } foreach (var nodeConnection in connection.ConnectedTo) @@ -85,7 +85,7 @@ namespace Barotrauma public void LoadConnections(XElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { int id = subElement.GetAttributeInt("i", -1); string? connectionType = subElement.GetAttributeString("type", null); @@ -170,9 +170,9 @@ namespace Barotrauma { if (connection.Type == NodeConnectionType.Value) { - if (connection.GetValue() != null) + if (connection.GetValue() is { } connValue) { - newElement.Add(new XAttribute(connection.Attribute?.ToLowerInvariant(), connection.GetValue())); + newElement.Add(new XAttribute(connection.Attribute.ToLowerInvariant(), connValue)); } } } @@ -269,8 +269,8 @@ namespace Barotrauma } } - Vector2 headerSize = GUI.SubHeadingFont.MeasureString(Name); - GUI.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor); + Vector2 headerSize = GUIStyle.SubHeadingFont.MeasureString(Name); + GUIStyle.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor); } public virtual void AddOption() @@ -445,13 +445,13 @@ namespace Barotrauma nodeValue = value; if (value is string str) { - WrappedText = TextManager.Get(str, true) is { } translated ? translated : str; + WrappedText = TextManager.Get(str) is { Loaded:true } translated ? translated.Value : str; } else { WrappedText = value?.ToString() ?? string.Empty; } - valueTextSize = GUI.SubHeadingFont.MeasureString(WrappedText); + valueTextSize = GUIStyle.SubHeadingFont.MeasureString(WrappedText); } } @@ -545,7 +545,7 @@ namespace Barotrauma width -= 16; } - valueText = ToolBox.WrapText(valueText, width, GUI.SubHeadingFont); + valueText = ToolBox.WrapText(valueText, width, GUIStyle.SubHeadingFont.Value); wrappedText = valueText; } } @@ -553,7 +553,7 @@ namespace Barotrauma public override Rectangle GetDrawRectangle() { Rectangle drawRectangle = Rectangle; - Vector2 size = GUI.SubHeadingFont.MeasureString(WrappedText); + Vector2 size = GUIStyle.SubHeadingFont.MeasureString(WrappedText ?? ""); drawRectangle.Height = (int) Math.Max(size.Y + 16, drawRectangle.Height); return drawRectangle; } @@ -564,7 +564,7 @@ namespace Barotrauma Vector2 pos = GetDrawRectangle().Location.ToVector2() + (GetDrawRectangle().Size.ToVector2() / 2) - (valueTextSize / 2); Rectangle drawRect = Rectangle; drawRect.Inflate(-1, -1); - GUI.DrawString(spriteBatch, pos, WrappedText, NodeConnection.GetPropertyColor(Type), font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, pos, WrappedText, NodeConnection.GetPropertyColor(Type), font: GUIStyle.SubHeadingFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index bc639deb0..890a4b1ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -13,7 +13,7 @@ using Directory = System.IO.Directory; namespace Barotrauma { - internal class EventEditorScreen : Screen + internal class EventEditorScreen : EditorScreen { private GUIFrame GuiFrame = null!; @@ -68,7 +68,7 @@ namespace Barotrauma // === LOAD PREFAB === // GUILayoutGroup loadEventLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, loadEventLayout), TextManager.Get("EventEditor.LoadEvent"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, loadEventLayout), TextManager.Get("EventEditor.LoadEvent"), font: GUIStyle.SubHeadingFont); GUILayoutGroup loadDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, loadEventLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown loadDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, loadDropdownLayout), elementCount: 10); @@ -77,7 +77,7 @@ namespace Barotrauma // === ADD ACTION === // GUILayoutGroup addActionLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, addActionLayout), TextManager.Get("EventEditor.AddAction"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, addActionLayout), TextManager.Get("EventEditor.AddAction"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addActionDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addActionLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addActionDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addActionDropdownLayout), elementCount: 10); @@ -85,7 +85,7 @@ namespace Barotrauma // === ADD VALUE === // GUILayoutGroup addValueLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, addValueLayout), TextManager.Get("EventEditor.AddValue"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, addValueLayout), TextManager.Get("EventEditor.AddValue"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addValueDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addValueLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addValueDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addValueDropdownLayout), elementCount: 7); @@ -93,15 +93,15 @@ namespace Barotrauma // === ADD SPECIAL === // GUILayoutGroup addSpecialLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, addSpecialLayout), TextManager.Get("EventEditor.AddSpecial"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, addSpecialLayout), TextManager.Get("EventEditor.AddSpecial"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addSpecialDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addSpecialLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addSpecialDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addSpecialDropdownLayout), elementCount: 1); GUIButton addSpecialButton = new GUIButton(RectTransform(0.2f, 1.0f, addSpecialDropdownLayout), TextManager.Get("EventEditor.Add")); // Add event prefabs with identifiers to the list - foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).Distinct()) + foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs().Where(prefab => !prefab.Identifier.IsEmpty).Distinct()) { - loadDropdown.AddItem(eventPrefab.Identifier, eventPrefab); + loadDropdown.AddItem(eventPrefab.Identifier.Value!, eventPrefab); } // Add all types that inherit the EventAction class @@ -183,7 +183,7 @@ namespace Barotrauma { if (!nameInput.Text.Contains(illegalChar)) { continue; } - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } @@ -195,7 +195,7 @@ namespace Barotrauma ToolBox.OpenFileWithShell(path); return true; }); - GUI.AddMessage($"XML exported to {path}", GUI.Style.Green); + GUI.AddMessage($"XML exported to {path}", GUIStyle.Green); return true; }; } @@ -206,7 +206,7 @@ namespace Barotrauma } else { - GUI.AddMessage("Unable to export because the project contains errors", GUI.Style.Red); + GUI.AddMessage("Unable to export because the project contains errors", GUIStyle.Red); } return true; @@ -219,15 +219,15 @@ namespace Barotrauma nodeList.Clear(); markedNodes.Clear(); selectedNodes.Clear(); - projectName = TextManager.Get("EventEditor.Unnamed"); + projectName = TextManager.Get("EventEditor.Unnamed").Value; return true; }); return true; } - public static GUIMessageBox AskForConfirmation(string header, string body, Func onConfirm) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm) { - string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons); // Cancel button @@ -274,7 +274,7 @@ namespace Barotrauma { if (!nameInput.Text.Contains(illegalChar)) { continue; } - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } @@ -283,7 +283,7 @@ namespace Barotrauma XElement save = SaveEvent(projectName); string filePath = System.IO.Path.Combine(directory, $"{projectName}.sevproj"); File.WriteAllText(Path.Combine(directory, $"{projectName}.sevproj"), save.ToString()); - GUI.AddMessage($"Project saved to {filePath}", GUI.Style.Green); + GUI.AddMessage($"Project saved to {filePath}", GUIStyle.Green); AskForConfirmation(TextManager.Get("EventEditor.TestPromptHeader"), TextManager.Get("EventEditor.TestPromptBody"), CreateTestSetupMenu); return true; @@ -342,11 +342,11 @@ namespace Barotrauma Vector2 spawnPos = Cam.WorldViewCenter; spawnPos.Y = -spawnPos.Y; - ConstructorInfo? constructor = type.GetConstructor(new Type[0]); + ConstructorInfo? constructor = type.GetConstructor(Array.Empty()); SpecialNode? newNode = null; if (constructor != null) { - newNode = constructor.Invoke(new object[0]) as SpecialNode; + newNode = constructor.Invoke(Array.Empty()) as SpecialNode; } if (newNode != null) { @@ -358,10 +358,10 @@ namespace Barotrauma return false; } - private void CreateNodes(XElement element, ref bool hadNodes, EditorNode? parent = null, int ident = 0) + private void CreateNodes(ContentXElement element, ref bool hadNodes, EditorNode? parent = null, int ident = 0) { EditorNode? lastNode = null; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { bool skip = true; switch (subElement.Name.ToString().ToLowerInvariant()) @@ -404,9 +404,9 @@ namespace Barotrauma hadNodes = false; } - XElement? parentElement = subElement.Parent; + var parentElement = subElement.Parent; - foreach (XElement xElement in subElement.Elements()) + foreach (var xElement in subElement.Elements()) { if (xElement.Name.ToString().ToLowerInvariant() == "option") { @@ -515,7 +515,7 @@ namespace Barotrauma public override void Select() { GUI.PreventPauseMenuToggle = false; - projectName = TextManager.Get("EventEditor.Unnamed"); + projectName = TextManager.Get("EventEditor.Unnamed").Value; base.Select(); } @@ -627,14 +627,14 @@ namespace Barotrauma private void Load(XElement saveElement) { nodeList.Clear(); - projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed")); + projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed").Value); foreach (XElement element in saveElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { case "nodes": { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { EditorNode? node = EditorNode.Load(subElement); if (node != null) @@ -647,7 +647,7 @@ namespace Barotrauma } case "allconnections": { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { int id = subElement.GetAttributeInt("i", -1); EditorNode? node = nodeList.Find(editorNode => editorNode.ID == id); @@ -702,31 +702,31 @@ namespace Barotrauma var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), msgBox.Content.RectTransform)); - new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.OutpostGenParams"), font: GUI.SubHeadingFont); - GUIDropDown paramInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, OutpostGenerationParams.Params.Count); - foreach (OutpostGenerationParams param in OutpostGenerationParams.Params) + new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.OutpostGenParams"), font: GUIStyle.SubHeadingFont); + GUIDropDown paramInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, OutpostGenerationParams.OutpostParams.Count()); + foreach (OutpostGenerationParams param in OutpostGenerationParams.OutpostParams) { - paramInput.AddItem(param.Identifier, param); + paramInput.AddItem(param.Identifier.Value!, param); } paramInput.OnSelected = (_, param) => { lastTestParam = param as OutpostGenerationParams; return true; }; - paramInput.SelectItem(lastTestParam ?? OutpostGenerationParams.Params.FirstOrDefault()); + paramInput.SelectItem(lastTestParam ?? OutpostGenerationParams.OutpostParams.FirstOrDefault()); - new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.LocationType"), font: GUI.SubHeadingFont); - GUIDropDown typeInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, LocationType.List.Count); - foreach (LocationType type in LocationType.List) + new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.LocationType"), font: GUIStyle.SubHeadingFont); + GUIDropDown typeInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, LocationType.Prefabs.Count()); + foreach (LocationType type in LocationType.Prefabs) { - typeInput.AddItem(type.Identifier, type); + typeInput.AddItem(type.Identifier.Value!, type); } typeInput.OnSelected = (_, type) => { lastTestType = type as LocationType; return true; }; - typeInput.SelectItem(lastTestType ?? LocationType.List.FirstOrDefault()); + typeInput.SelectItem(lastTestType ?? LocationType.Prefabs.FirstOrDefault()); // Cancel button msgBox.Buttons[0].OnClicked = (button, o) => @@ -783,8 +783,8 @@ namespace Barotrauma if (type.IsEnum) { Array enums = Enum.GetValues(type); - GUIDropDown valueInput = new GUIDropDown(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString(), enums.Length); - foreach (object? @enum in enums) { valueInput.AddItem(@enum?.ToString(), @enum); } + GUIDropDown valueInput = new GUIDropDown(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? "", enums.Length); + foreach (object? @enum in enums) { valueInput.AddItem(@enum?.ToString() ?? "", @enum); } valueInput.OnSelected += (component, o) => { @@ -859,22 +859,22 @@ namespace Barotrauma private bool TestEvent(OutpostGenerationParams? param, LocationType? type) { - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.HasTag(SubmarineTag.Shuttle)); + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.HasTag(SubmarineTag.Shuttle)) ?? throw new NullReferenceException("Could not test event: There are no shuttles available."); XElement? eventXml = ExportXML(); EventPrefab? prefab; if (eventXml != null) { - prefab = new EventPrefab(eventXml); + prefab = new EventPrefab(eventXml.FromPackage(null), null); } else { - GUI.AddMessage("Unable to open test enviroment because the event contains errors.", GUI.Style.Red); + GUI.AddMessage("Unable to open test enviroment because the event contains errors.", GUIStyle.Red); return false; } GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); - TestGameMode gameMode = (TestGameMode) gameSession.GameMode; + TestGameMode gameMode = ((TestGameMode?)gameSession.GameMode) ?? throw new InvalidCastException(); gameMode.SpawnOutpost = true; gameMode.OutpostParams = param; @@ -930,8 +930,8 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(DrawnTooltip)) { - string tooltip = ToolBox.WrapText(DrawnTooltip, 256.0f, GUI.SmallFont); - GUI.DrawString(spriteBatch, PlayerInput.MousePosition + new Vector2(32, 32), tooltip, Color.White, Color.Black * 0.8f, 4, GUI.SmallFont); + string tooltip = ToolBox.WrapText(DrawnTooltip, 256.0f, GUIStyle.SmallFont.Value); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + new Vector2(32, 32), tooltip, Color.White, Color.Black * 0.8f, 4, GUIStyle.SmallFont); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs index d7be315f2..d5814d94a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs @@ -55,7 +55,14 @@ namespace Barotrauma set { optionText = value; - actualValue = WrappedValue = TextManager.Get(value, true) is { } translated ? translated : value; + if (value is string) + { + actualValue = WrappedValue = TextManager.Get(value).Fallback(value).Value; + } + else + { + actualValue = WrappedValue = value; + } } } @@ -74,7 +81,7 @@ namespace Barotrauma overrideValue = value; if (value is string str) { - actualValue = WrappedValue = TextManager.Get(str, true) is { } translated ? translated : str; + actualValue = WrappedValue = TextManager.Get(str).Fallback(str).Value; } else { @@ -96,13 +103,13 @@ namespace Barotrauma wrappedValue = null; return; } - Vector2 textSize = GUI.SmallFont.MeasureString(valueText); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(valueText); bool wasWrapped = false; while (textSize.X > 96) { wasWrapped = true; valueText = $"{valueText}...".Substring(0, valueText.Length - 4); - textSize = GUI.SmallFont.MeasureString($"{valueText}..."); + textSize = GUIStyle.SmallFont.MeasureString($"{valueText}..."); } if (wasWrapped) @@ -178,20 +185,20 @@ namespace Barotrauma Point pos = GetRenderPos(parentRectangle, yOffset); DrawRectangle = new Rectangle(pos, new Point(16, 16)); GUI.DrawRectangle(spriteBatch, DrawRectangle, bgColor, isFilled: true); - GUI.DrawRectangle(spriteBatch, DrawRectangle, EndConversation ? GUI.Style.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); + GUI.DrawRectangle(spriteBatch, DrawRectangle, EndConversation ? GUIStyle.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); string label = string.IsNullOrWhiteSpace(Attribute) ? Type.Label : Attribute; - float xPos = parentRectangle.Center.X > pos.X ? 24 : -8 - GUI.SmallFont.MeasureString(label).X; + float xPos = parentRectangle.Center.X > pos.X ? 24 : -8 - GUIStyle.SmallFont.MeasureString(label).X; if (Type != NodeConnectionType.Out) { - Vector2 size = GUI.SmallFont.MeasureString(label); + Vector2 size = GUIStyle.SmallFont.MeasureString(label); Vector2 positon = new Vector2(pos.X + xPos, pos.Y); Rectangle bgRect = new Rectangle(positon.ToPoint(), size.ToPoint()); bgRect.Inflate(4, 4); GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.6f, isFilled: true); - GUI.DrawString(spriteBatch, positon, label, GetPropertyColor(ValueType), font: GUI.SmallFont); + GUI.DrawString(spriteBatch, positon, label, GetPropertyColor(ValueType), font: GUIStyle.SmallFont); Vector2 mousePos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); mousePos.Y = -mousePos.Y; @@ -250,13 +257,13 @@ namespace Barotrauma { float camZoom = Screen.Selected is EventEditorScreen eventEditor ? eventEditor.Cam.Zoom : 1.0f; Rectangle valueRect = new Rectangle((int)pos.X, (int)pos.Y, 96, 20); - Vector2 textSize = GUI.SmallFont.MeasureString(text); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Vector2 position = valueRect.Location.ToVector2() + valueRect.Size.ToVector2() / 2 - textSize / 2; Rectangle drawRect = valueRect; drawRect.Inflate(4, 4); GUI.DrawRectangle(spriteBatch, drawRect, new Color(50, 50, 50), isFilled: true); - GUI.DrawRectangle(spriteBatch, drawRect, EndConversation ? GUI.Style.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); - GUI.DrawString(spriteBatch, position, text, GetPropertyColor(ValueType), font: GUI.SmallFont); + GUI.DrawRectangle(spriteBatch, drawRect, EndConversation ? GUIStyle.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); + GUI.DrawString(spriteBatch, position, text, GetPropertyColor(ValueType), font: GUIStyle.SmallFont); DrawRectangle = Rectangle.Union(DrawRectangle, drawRect); if (!string.IsNullOrWhiteSpace(fullText)) @@ -304,7 +311,7 @@ namespace Barotrauma points[2].Y -= points[2].Y - points[1].Y; } - Color drawColor = Parent is ValueNode ? GetPropertyColor(ValueType) : GUI.Style.Red; + Color drawColor = Parent is ValueNode ? GetPropertyColor(ValueType) : GUIStyle.Red; if (overrideColor != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 9a0675524..e58af763e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -11,6 +11,8 @@ namespace Barotrauma { partial class GameScreen : Screen { + public override bool IsEditor => GameMain.GameSession?.GameMode is TestGameMode; + private RenderTarget2D renderTargetBackground; private RenderTarget2D renderTarget; private RenderTarget2D renderTargetWater; @@ -142,11 +144,11 @@ namespace Barotrauma Vector2 position = Submarine.MainSubs[i].SubBody != null ? Submarine.MainSubs[i].WorldPosition : Submarine.MainSubs[i].HiddenSubPosition; - Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUI.Style.Red * 0.5f; + Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUIStyle.Red * 0.5f; GUI.DrawIndicator( spriteBatch, position, cam, Math.Max(Submarine.MainSub.Borders.Width, Submarine.MainSub.Borders.Height), - GUI.SubmarineIcon, indicatorColor); + GUIStyle.SubmarineLocationIcon.Value.Sprite, indicatorColor); } } @@ -390,14 +392,14 @@ namespace Barotrauma float BlurStrength = 0.0f; float DistortStrength = 0.0f; - Vector3 chromaticAberrationStrength = GameMain.Config.ChromaticAberrationEnabled ? + Vector3 chromaticAberrationStrength = GameSettings.CurrentConfig.Graphics.ChromaticAberration ? new Vector3(-0.02f, -0.01f, 0.0f) : Vector3.Zero; if (Character.Controlled != null) { BlurStrength = Character.Controlled.BlurStrength * 0.005f; DistortStrength = Character.Controlled.DistortStrength; - if (GameMain.Config.EnableRadialDistortion) + if (GameSettings.CurrentConfig.Graphics.RadialDistortion) { chromaticAberrationStrength -= Vector3.One * Character.Controlled.RadialDistortStrength; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 061d6e262..71bc8cab5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -16,13 +16,9 @@ using Barotrauma.IO; namespace Barotrauma { - class LevelEditorScreen : Screen + class LevelEditorScreen : EditorScreen { - private readonly Camera cam; - public override Camera Cam - { - get { return cam; } - } + public override Camera Cam { get; } private readonly GUIFrame leftPanel, rightPanel, bottomPanel, topPanel; @@ -46,7 +42,7 @@ namespace Barotrauma public LevelEditorScreen() { - cam = new Camera() + Cam = new Camera() { MinZoom = 0.01f, MaxZoom = 1.0f @@ -69,7 +65,7 @@ namespace Barotrauma return true; }; - var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUI.SubHeadingFont); + var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUIStyle.SubHeadingFont); ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); ruinParamsList.OnSelected += (GUIComponent component, object obj) => @@ -78,7 +74,7 @@ namespace Barotrauma return true; }; - var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUI.SubHeadingFont); + var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUIStyle.SubHeadingFont); caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); caveParamsList.OnSelected += (GUIComponent component, object obj) => @@ -87,7 +83,7 @@ namespace Barotrauma return true; }; - var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUI.SubHeadingFont); + var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(ruinTitle, caveTitle, outpostTitle); outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); @@ -185,13 +181,13 @@ namespace Barotrauma levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; Level.Generate(levelData, mirror: mirrorLevel.Selected); GameMain.LightManager.AddLight(pointerLightSource); - if (!wasLevelLoaded || cam.Position.X < 0 || cam.Position.Y < 0 || cam.Position.Y > Level.Loaded.Size.X || cam.Position.Y > Level.Loaded.Size.Y) + if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) { - cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); + Cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); } foreach (GUITextBlock param in paramsList.Content.Children) { - param.TextColor = param.UserData == selectedParams ? GUI.Style.Green : param.Style.TextColor; + param.TextColor = param.UserData == selectedParams ? GUIStyle.Green : param.Style.TextColor; } seedBox.Deselect(); return true; @@ -218,14 +214,13 @@ namespace Barotrauma Submarine.MainSub.Remove(); } - //TODO: hacky workaround to check for wrecks and outposts, refactor SubmarineInfo and ContentType at some point - var nonPlayerFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList(); - nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost)); - nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule)); - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(GameMain.Config.QuickStartSubmarineName, StringComparison.InvariantCultureIgnoreCase)); - subInfo ??= SubmarineInfo.SavedSubmarines.GetRandom(s => + var nonPlayerFiles = ContentPackageManager.EnabledPackages.All.SelectMany(p => p + .GetFiles() + .Where(f => !(f is SubmarineFile))).ToArray(); + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == GameSettings.CurrentConfig.QuickStartSub); + subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && - !nonPlayerFiles.Any(f => f.Path.CleanUpPath().Equals(s.FilePath.CleanUpPath(), StringComparison.InvariantCultureIgnoreCase))); + !nonPlayerFiles.Any(f => f.Path == s.FilePath)); GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); gameSession.StartRound(Level.Loaded.LevelData); (gameSession.GameMode as TestGameMode).OnRoundEnd = () => @@ -309,7 +304,7 @@ namespace Barotrauma foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) }, - genParams.Identifier) + genParams.Identifier.Value) { Padding = Vector4.Zero, UserData = genParams @@ -354,7 +349,7 @@ namespace Barotrauma editorContainer.ClearChildren(); outpostParamsList.Content.ClearChildren(); - foreach (OutpostGenerationParams genParams in OutpostGenerationParams.Params) + foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -373,7 +368,7 @@ namespace Barotrauma int objectsPerRow = (int)Math.Ceiling(levelObjectList.Content.Rect.Width / Math.Max(100 * GUI.Scale, 100)); float relWidth = 1.0f / objectsPerRow; - foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.Prefabs) { var frame = new GUIFrame(new RectTransform( new Vector2(relWidth, relWidth * ((float)levelObjectList.Content.Rect.Width / levelObjectList.Content.Rect.Height)), @@ -384,7 +379,7 @@ namespace Barotrauma var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: ToolBox.LimitString(levelObjPrefab.Name, GUI.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUI.SmallFont) + text: ToolBox.LimitString(levelObjPrefab.Name, GUIStyle.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false, ToolTip = levelObjPrefab.Name @@ -414,7 +409,7 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), - TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Identifier), textAlignment: Alignment.Center); + TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Identifier.Value), textAlignment: Alignment.Center); new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) { MinValueFloat = 0, @@ -443,14 +438,14 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any" }; - foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier); } + HashSet availableLocationTypes = new HashSet { "any".ToIdentifier() }; + foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), - text: string.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); - foreach (string locationType in availableLocationTypes) + text: LocalizedString.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true); + foreach (Identifier locationType in availableLocationTypes) { - locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType); if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) { locationTypeDropDown.SelectItem(locationType); @@ -463,7 +458,7 @@ namespace Barotrauma locationTypeDropDown.OnSelected += (_, __) => { - outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); + outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); return true; }; @@ -473,13 +468,13 @@ namespace Barotrauma // module count ------------------------- - var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUI.SubHeadingFont); + var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUIStyle.SubHeadingFont); outpostParamsEditor.AddCustomContent(moduleLabel, 100); - foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) + foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) { var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key), textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key.Value), textAlignment: Alignment.CenterLeft); new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) { MinValueInt = 0, @@ -502,24 +497,24 @@ namespace Barotrauma var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); - HashSet availableFlags = new HashSet(); - foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + HashSet availableFlags = new HashSet(); + foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } + foreach (Identifier flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } } var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 0.8f), addModuleCountGroup.RectTransform), text: TextManager.Get("leveleditor.addmoduletype")); - foreach (string flag in availableFlags) + foreach (Identifier flag in availableFlags) { - if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key.Equals(flag, StringComparison.OrdinalIgnoreCase))) { continue; } - moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); + if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key == flag)) { continue; } + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); } moduleTypeDropDown.OnSelected += (_, userdata) => { - outpostGenerationParams.SetModuleCount(userdata as string, 1); + outpostGenerationParams.SetModuleCount((Identifier)userdata, 1); outpostParamsList.Select(outpostParamsList.SelectedData); return true; }; @@ -532,14 +527,12 @@ namespace Barotrauma { editorContainer.ClearChildren(); - var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, levelObjectPrefab, false, true, elementHeight: 20, titleFont: GUI.LargeFont); + var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, levelObjectPrefab, false, true, elementHeight: 20, titleFont: GUIStyle.LargeFont); if (selectedParams != null) { - List availableIdentifiers = new List(); - { - if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } - } + List availableIdentifiers = new List(); + if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } foreach (var caveParam in CaveGenerationParams.CaveParams) { if (selectedParams != null && caveParam.GetCommonness(selectedParams, abyss: false) <= 0.0f) { continue; } @@ -547,7 +540,7 @@ namespace Barotrauma } availableIdentifiers.Reverse(); - foreach (string paramsId in availableIdentifiers) + foreach (Identifier paramsId in availableIdentifiers) { var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, isHorizontal: false, childAnchor: Anchor.TopCenter) @@ -556,7 +549,7 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), - TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId), textAlignment: Alignment.Center); + TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId.Value), textAlignment: Alignment.Center); new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) { MinValueFloat = 0, @@ -599,7 +592,7 @@ namespace Barotrauma } //child object editing new GUITextBlock(new RectTransform(new Point(editor.Rect.Width, 40), editorContainer.Content.RectTransform), - TextManager.Get("leveleditor.childobjects"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomCenter); + TextManager.Get("leveleditor.childobjects"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); foreach (LevelObjectPrefab.ChildObject childObj in levelObjectPrefab.ChildObjects) { var childObjFrame = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, 30))); @@ -610,7 +603,7 @@ namespace Barotrauma }; var selectedChildObj = childObj; var dropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), paddedFrame.RectTransform), elementCount: 10, selectMultiple: true); - foreach (LevelObjectPrefab objPrefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab objPrefab in LevelObjectPrefab.Prefabs) { dropdown.AddItem(objPrefab.Name, objPrefab); if (childObj.AllowedNames.Contains(objPrefab.Name)) { dropdown.SelectItem(objPrefab); } @@ -669,7 +662,7 @@ namespace Barotrauma //light editing new GUITextBlock(new RectTransform(new Point(editor.Rect.Width, 40), editorContainer.Content.RectTransform), - TextManager.Get("leveleditor.lightsources"), textAlignment: Alignment.BottomCenter, font: GUI.SubHeadingFont); + TextManager.Get("leveleditor.lightsources"), textAlignment: Alignment.BottomCenter, font: GUIStyle.SubHeadingFont); foreach (LightSourceParams lightSourceParams in selectedLevelObject.LightSourceParams) { new SerializableEntityEditor(editorContainer.Content.RectTransform, lightSourceParams, inGame: false, showName: true); @@ -696,9 +689,9 @@ namespace Barotrauma { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; float commonness = levelObj.GetCommonness(selectedParams); - levelObjFrame.Color = commonness > 0.0f ? GUI.Style.Green * 0.4f : Color.Transparent; - levelObjFrame.SelectedColor = commonness > 0.0f ? GUI.Style.Green * 0.6f : Color.White * 0.5f; - levelObjFrame.HoverColor = commonness > 0.0f ? GUI.Style.Green * 0.7f : Color.White * 0.6f; + levelObjFrame.Color = commonness > 0.0f ? GUIStyle.Green * 0.4f : Color.Transparent; + levelObjFrame.SelectedColor = commonness > 0.0f ? GUIStyle.Green * 0.6f : Color.White * 0.5f; + levelObjFrame.HoverColor = commonness > 0.0f ? GUIStyle.Green * 0.7f : Color.White * 0.6f; levelObjFrame.GetAnyChild().Color = commonness > 0.0f ? Color.White : Color.DarkGray; if (commonness <= 0.0f) @@ -735,20 +728,20 @@ namespace Barotrauma { if (lightingEnabled.Selected) { - GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); + GameMain.LightManager.RenderLightMap(graphics, spriteBatch, Cam); } graphics.Clear(Color.Black); if (Level.Loaded != null) { - Level.Loaded.DrawBack(graphics, spriteBatch, cam); - Level.Loaded.DrawFront(spriteBatch, cam); - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, transformMatrix: cam.Transform); - Level.Loaded.DrawDebugOverlay(spriteBatch, cam); + Level.Loaded.DrawBack(graphics, spriteBatch, Cam); + Level.Loaded.DrawFront(spriteBatch, Cam); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, transformMatrix: Cam.Transform); + Level.Loaded.DrawDebugOverlay(spriteBatch, Cam); Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); - GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / cam.Zoom)); + GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / Cam.Zoom)); for (int i = 0; i < Level.Loaded.Tunnels.Count; i++) { @@ -758,21 +751,21 @@ namespace Barotrauma { Vector2 start = new Vector2(tunnel.Nodes[j - 1].X, -tunnel.Nodes[j - 1].Y); Vector2 end = new Vector2(tunnel.Nodes[j].X, -tunnel.Nodes[j].Y); - GUI.DrawLine(spriteBatch, start, end, tunnelColor, width: (int)(2.0f / cam.Zoom)); + GUI.DrawLine(spriteBatch, start, end, tunnelColor, width: (int)(2.0f / Cam.Zoom)); } } foreach (Level.InterestingPosition interestingPos in Level.Loaded.PositionsOfInterest) { - if (interestingPos.Position.X < cam.WorldView.X || interestingPos.Position.X > cam.WorldView.Right || - interestingPos.Position.Y > cam.WorldView.Y || interestingPos.Position.Y < cam.WorldView.Y - cam.WorldView.Height) + if (interestingPos.Position.X < Cam.WorldView.X || interestingPos.Position.X > Cam.WorldView.Right || + interestingPos.Position.Y > Cam.WorldView.Y || interestingPos.Position.Y < Cam.WorldView.Y - Cam.WorldView.Height) { continue; } Vector2 pos = new Vector2(interestingPos.Position.X, -interestingPos.Position.Y); - spriteBatch.DrawCircle(pos, 500, 6, Color.White * 0.5f, thickness: (int)(2 / cam.Zoom)); - GUI.DrawString(spriteBatch, pos, interestingPos.PositionType.ToString(), Color.White, font: GUI.LargeFont); + spriteBatch.DrawCircle(pos, 500, 6, Color.White * 0.5f, thickness: (int)(2 / Cam.Zoom)); + GUI.DrawString(spriteBatch, pos, interestingPos.PositionType.ToString(), Color.White, font: GUIStyle.LargeFont); } // TODO: Improve this temporary level editor debug solution (or remove it) @@ -785,17 +778,17 @@ namespace Barotrauma foreach (var resource in location.Resources) { Vector2 resourcePos = new Vector2(resource.Position.X, -resource.Position.Y); - spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / cam.Zoom)); - GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUI.LargeFont); + spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / Cam.Zoom)); + GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUIStyle.LargeFont); var dist = Vector2.Distance(resourcePos, pathPointPos); var lineStartPos = Vector2.Lerp(resourcePos, pathPointPos, 110 / dist); var lineEndPos = Vector2.Lerp(pathPointPos, resourcePos, 310 / dist); - GUI.DrawLine(spriteBatch, lineStartPos, lineEndPos, Color.DarkGreen * 0.5f, width: (int)(2 / cam.Zoom)); + GUI.DrawLine(spriteBatch, lineStartPos, lineEndPos, Color.DarkGreen * 0.5f, width: (int)(2 / Cam.Zoom)); } } var color = pathPoint.ShouldContainResources ? Color.DarkGreen : Color.DarkRed; - spriteBatch.DrawCircle(pathPointPos, 300, 6, color * 0.5f, thickness: (int)(2 / cam.Zoom)); - GUI.DrawString(spriteBatch, pathPointPos, "Path Point\n" + pathPoint.Id, color, font: GUI.LargeFont); + spriteBatch.DrawCircle(pathPointPos, 300, 6, color * 0.5f, thickness: (int)(2 / Cam.Zoom)); + GUI.DrawString(spriteBatch, pathPointPos, "Path Point\n" + pathPoint.Id, color, font: GUIStyle.LargeFont); } /*for (int i = 0; i < Level.Loaded.distanceField.Count; i++) @@ -833,24 +826,24 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); if (Level.Loaded != null) { - float crushDepthScreen = cam.WorldToScreen(new Vector2(0.0f, -Level.Loaded.CrushDepth)).Y; + float crushDepthScreen = Cam.WorldToScreen(new Vector2(0.0f, -Level.Loaded.CrushDepth)).Y; if (crushDepthScreen > 0.0f && crushDepthScreen < GameMain.GraphicsHeight) { - GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), GUI.Style.Red * 0.25f, width: 5); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), "Crush depth", GUI.Style.Red, backgroundColor: Color.Black); + GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), GUIStyle.Red * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), "Crush depth", GUIStyle.Red, backgroundColor: Color.Black); } - float abyssStartScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; + float abyssStartScreen = Cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; if (abyssStartScreen > 0.0f && abyssStartScreen < GameMain.GraphicsHeight) { - GUI.DrawLine(spriteBatch, new Vector2(0, abyssStartScreen), new Vector2(GameMain.GraphicsWidth, abyssStartScreen), GUI.Style.Blue * 0.25f, width: 5); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssStartScreen), "Abyss start", GUI.Style.Blue, backgroundColor: Color.Black); + GUI.DrawLine(spriteBatch, new Vector2(0, abyssStartScreen), new Vector2(GameMain.GraphicsWidth, abyssStartScreen), GUIStyle.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssStartScreen), "Abyss start", GUIStyle.Blue, backgroundColor: Color.Black); } - float abyssEndScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Y)).Y; + float abyssEndScreen = Cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Y)).Y; if (abyssEndScreen > 0.0f && abyssEndScreen < GameMain.GraphicsHeight) { - GUI.DrawLine(spriteBatch, new Vector2(0, abyssEndScreen), new Vector2(GameMain.GraphicsWidth, abyssEndScreen), GUI.Style.Blue * 0.25f, width: 5); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssEndScreen), "Abyss end", GUI.Style.Blue, backgroundColor: Color.Black); + GUI.DrawLine(spriteBatch, new Vector2(0, abyssEndScreen), new Vector2(GameMain.GraphicsWidth, abyssEndScreen), GUIStyle.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssEndScreen), "Abyss end", GUIStyle.Blue, backgroundColor: Color.Black); } } GUI.Draw(Cam, spriteBatch); @@ -863,17 +856,17 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - item?.GetComponent()?.Update((float)deltaTime, cam); + item?.GetComponent()?.Update((float)deltaTime, Cam); } } GameMain.LightManager?.Update((float)deltaTime); - pointerLightSource.Position = cam.ScreenToWorld(PlayerInput.MousePosition); + pointerLightSource.Position = Cam.ScreenToWorld(PlayerInput.MousePosition); pointerLightSource.Enabled = cursorLightEnabled.Selected; pointerLightSource.IsBackground = true; - cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null); - cam.UpdateTransform(); - Level.Loaded?.Update((float)deltaTime, cam); + Cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null); + Cam.UpdateTransform(); + Level.Loaded?.Update((float)deltaTime, Cam); if (editingSprite != null) { @@ -888,7 +881,7 @@ namespace Barotrauma Indent = true, NewLineOnAttributes = true }; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -899,7 +892,7 @@ namespace Barotrauma { if (element.IsOverride()) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -915,14 +908,14 @@ namespace Barotrauma break; } } - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); } } - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -933,7 +926,7 @@ namespace Barotrauma { if (element.IsOverride()) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string id = subElement.GetAttributeString("identifier", null) ?? subElement.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -949,7 +942,7 @@ namespace Barotrauma break; } } - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -957,22 +950,22 @@ namespace Barotrauma } settings.NewLineOnAttributes = false; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.Prefabs) { foreach (XElement element in doc.Root.Elements()) { - string identifier = element.GetAttributeString("identifier", null); - if (!identifier.Equals(levelObjPrefab.Identifier, StringComparison.OrdinalIgnoreCase)) { continue; } + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + if (identifier != levelObjPrefab.Identifier) { continue; } levelObjPrefab.Save(element); break; } } - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -984,7 +977,7 @@ namespace Barotrauma private void Serialize(LevelGenerationParams genParams) { - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -1006,7 +999,7 @@ namespace Barotrauma NewLineOnAttributes = true }; - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -1043,7 +1036,7 @@ namespace Barotrauma public GUIMessageBox Create() { var box = new GUIMessageBox(TextManager.Get("leveleditor.createlevelobj"), string.Empty, - new string[] { TextManager.Get("cancel"), TextManager.Get("done") }, new Vector2(0.5f, 0.8f)); + new LocalizedString[] { TextManager.Get("cancel"), TextManager.Get("done") }, new Vector2(0.5f, 0.8f)); box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = 20; @@ -1057,14 +1050,15 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform), TextManager.Get("leveleditor.levelobjtexturepath")) { CanBeFocused = false }; var texturePathBox = new GUITextBox(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform)); - foreach (LevelObjectPrefab prefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab prefab in LevelObjectPrefab.Prefabs) { if (prefab.Sprites.FirstOrDefault() == null) continue; - texturePathBox.Text = Path.GetDirectoryName(prefab.Sprites.FirstOrDefault().FilePath); + texturePathBox.Text = Path.GetDirectoryName(prefab.Sprites.FirstOrDefault().FilePath.Value); break; } - newPrefab = new LevelObjectPrefab(null); + //this is nasty :( + newPrefab = new LevelObjectPrefab(null, null, Identifier.Empty); new SerializableEntityEditor(listBox.Content.RectTransform, newPrefab, false, false); @@ -1078,51 +1072,62 @@ namespace Barotrauma { if (string.IsNullOrEmpty(nameBox.Text)) { - nameBox.Flash(GUI.Style.Red); - GUI.AddMessage(TextManager.Get("leveleditor.levelobjnameempty"), GUI.Style.Red); + nameBox.Flash(GUIStyle.Red); + GUI.AddMessage(TextManager.Get("leveleditor.levelobjnameempty"), GUIStyle.Red); return false; } - if (LevelObjectPrefab.List.Any(obj => obj.Identifier.Equals(nameBox.Text, StringComparison.OrdinalIgnoreCase))) + if (LevelObjectPrefab.Prefabs.Any(obj => obj.Identifier == nameBox.Text)) { - nameBox.Flash(GUI.Style.Red); - GUI.AddMessage(TextManager.Get("leveleditor.levelobjnametaken"), GUI.Style.Red); + nameBox.Flash(GUIStyle.Red); + GUI.AddMessage(TextManager.Get("leveleditor.levelobjnametaken"), GUIStyle.Red); return false; } if (!File.Exists(texturePathBox.Text)) { - texturePathBox.Flash(GUI.Style.Red); - GUI.AddMessage(TextManager.Get("leveleditor.levelobjtexturenotfound"), GUI.Style.Red); + texturePathBox.Flash(GUIStyle.Red); + GUI.AddMessage(TextManager.Get("leveleditor.levelobjtexturenotfound"), GUIStyle.Red); return false; } - newPrefab.Identifier = nameBox.Text; - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true }; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { continue; } - var newElement = new XElement(newPrefab.Identifier); - newPrefab.Save(newElement); - newElement.Add(new XElement("Sprite", - new XAttribute("texture", texturePathBox.Text), - new XAttribute("sourcerect", "0,0,100,100"), - new XAttribute("origin", "0.5,0.5"))); - doc.Root.Add(newElement); - using (var writer = XmlWriter.Create(configFile.Path, settings)) - { - doc.WriteTo(writer); - writer.Flush(); - } - // Recreate the prefab so that the sprite loads correctly: TODO: consider a better way to do this - newPrefab = new LevelObjectPrefab(newElement); - break; - } + var newElement = new XElement(nameBox.Text); + newPrefab.Save(newElement); + newElement.Add(new XElement("Sprite", + new XAttribute("texture", texturePathBox.Text), + new XAttribute("sourcerect", "0,0,100,100"), + new XAttribute("origin", "0.5,0.5"))); - LevelObjectPrefab.List.Add(newPrefab); + // Create a new mod for the purpose of providing this new prefab + #warning TODO: add a clear way to tack it into an existing content package? + string modDir = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); + Directory.CreateDirectory(modDir); + + string fileListPath = Path.Combine(modDir, ContentPackage.FileListFileName); + string prefabFilePath = Path.Combine(modDir, $"{nameBox.Text}.xml"); + + var newMod = new ModProject { Name = nameBox.Text }; + var newFile = ModProject.File.FromPath(prefabFilePath); + newMod.AddFile(newFile); + + XDocument fileListDoc = newMod.ToXDocument(); + Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)); + using (XmlWriter writer = XmlWriter.Create(fileListPath, settings)) { fileListDoc.Save(writer); } + + XDocument prefabDoc = new XDocument(); + var prefabFileRoot = new XElement("LevelObjects"); + prefabFileRoot.Add(newElement); + prefabDoc.Add(prefabFileRoot); + using (XmlWriter writer = XmlWriter.Create(prefabFilePath, settings)) { prefabDoc.Save(writer); } + + ContentPackageManager.UpdateContentPackageList(); + + var newRegularList = ContentPackageManager.EnabledPackages.Regular.ToList(); + newRegularList.Add(ContentPackageManager.RegularPackages.First(p => p.Name == nameBox.Text)); + ContentPackageManager.EnabledPackages.SetRegular(newRegularList); + GameMain.LevelEditorScreen.UpdateLevelObjectsList(); box.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 5319a5ca1..0ff737f3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -9,11 +9,13 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; +using System.Data.Common; using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Net; using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.Steam; @@ -21,7 +23,7 @@ namespace Barotrauma { class MainMenuScreen : Screen { - public enum Tab + private enum Tab { NewGame = 0, LoadGame = 1, @@ -38,20 +40,20 @@ namespace Barotrauma private readonly GUIComponent buttonsParent; - private readonly GUIFrame[] menuTabs; + private readonly Dictionary menuTabs; private SinglePlayerCampaignSetupUI campaignSetupUI; - private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; + private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; - private readonly GUIFrame downloadingModsContainer, enableModsContainer; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; private GUIImage playstyleBanner; private GUITextBlock playstyleDescription; - private GUIComponent remoteContentContainer; + private const string RemoteContentUrl = "http://www.barotraumagame.com/gamedata/"; + private readonly GUIComponent remoteContentContainer; private XDocument remoteContentDoc; private Tab selectedTab = Tab.Empty; @@ -62,28 +64,21 @@ namespace Barotrauma private readonly CreditsPlayer creditsPlayer; -#if OSX - private bool firstLoadOnMac = true; -#endif + public static readonly Queue WorkshopItemsToUpdate = new Queue(); -#region Creation + #region Creation public MainMenuScreen(GameMain game) { GameMain.Instance.ResolutionChanged += () => { - if (Selected == this && selectedTab == Tab.Settings) - { - GameMain.Config.ResetSettingsFrame(); - SelectTab(Tab.Settings); - } CreateHostServerFields(); CreateCampaignSetupUI(); if (remoteContentDoc?.Root != null) { remoteContentContainer.ClearChildren(); - foreach (XElement subElement in remoteContentDoc.Root.Elements()) + foreach (var subElement in remoteContentDoc.Root.Elements()) { - GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); } } }; @@ -118,9 +113,9 @@ namespace Barotrauma var doc = XMLExtensions.TryLoadXml("Content/UI/MenuContent.xml"); if (doc?.Root != null) { - foreach (XElement subElement in doc?.Root.Elements()) + foreach (var subElement in doc?.Root.Elements()) { - GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); } } #else @@ -144,7 +139,7 @@ namespace Barotrauma var campaignNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: campaignHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, labelHeight), campaignNavigation.RectTransform), - TextManager.Get("CampaignLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true }; + TextManager.Get("CampaignLabel"), textAlignment: Alignment.Left, font: GUIStyle.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = ForceUpperCase.Yes }; var campaignButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: campaignNavigation.RectTransform), style: "MainMenuGUIFrame"); @@ -156,7 +151,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("TutorialButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.Tutorials, OnClicked = (tb, userdata) => { @@ -167,7 +162,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("LoadGameButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.LoadGame, OnClicked = (tb, userdata) => { @@ -178,7 +173,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("NewGameButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.NewGame, OnClicked = (tb, userdata) => { @@ -201,7 +196,7 @@ namespace Barotrauma var multiplayerNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: multiplayerHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, labelHeight), multiplayerNavigation.RectTransform), - TextManager.Get("MultiplayerLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true }; + TextManager.Get("MultiplayerLabel"), textAlignment: Alignment.Left, font: GUIStyle.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = ForceUpperCase.Yes }; var multiplayerButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: multiplayerNavigation.RectTransform), style: "MainMenuGUIFrame"); @@ -213,7 +208,7 @@ namespace Barotrauma joinServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), multiplayerList.RectTransform), TextManager.Get("JoinServerButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.JoinServer, OnClicked = (tb, userdata) => { @@ -223,7 +218,7 @@ namespace Barotrauma }; hostServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), multiplayerList.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.HostServer, OnClicked = (tb, userdata) => { @@ -246,7 +241,7 @@ namespace Barotrauma var customizeNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: customizeHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, labelHeight), customizeNavigation.RectTransform), - TextManager.Get("CustomizeLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true }; + TextManager.Get("CustomizeLabel"), textAlignment: Alignment.Left, font: GUIStyle.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = ForceUpperCase.Yes }; var customizeButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: customizeNavigation.RectTransform), style: "MainMenuGUIFrame"); @@ -257,36 +252,18 @@ namespace Barotrauma }; #if USE_STEAM - var steamWorkshopButtonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), style: null); - - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), steamWorkshopButtonContainer.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, - Enabled = false, + ForceUpperCase = ForceUpperCase.Yes, + Enabled = true, UserData = Tab.SteamWorkshop, OnClicked = SelectTab }; - - downloadingModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), steamWorkshopButtonContainer.RectTransform, - Anchor.CenterRight, Pivot.CenterLeft) - { RelativeOffset = new Vector2(0.3f, 0.0f) }, - "MainMenuNotifBackground", Color.Yellow) - { - CanBeFocused = false, - UserData = "workshopnotif", - Visible = false - }; - new GUITextBlock(new RectTransform(Vector2.One * 0.9f, downloadingModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, - TextManager.Get("ModsDownloadingNotif"), Color.Black) - { - CanBeFocused = false, - }; - #endif - + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.SubmarineEditor, OnClicked = (tb, userdata) => { @@ -297,7 +274,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("CharacterEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.CharacterEditor, OnClicked = (tb, userdata) => { @@ -329,51 +306,37 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, settingsButtonContainer.RectTransform), TextManager.Get("SettingsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.Settings, OnClicked = SelectTab }; - - enableModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), settingsButtonContainer.RectTransform, - Anchor.CenterRight, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.5f, 0.0f) }, - "MainMenuNotifBackground", Color.Yellow) - { - CanBeFocused = false, - UserData = "settingsnotif", - Visible = false - }; - new GUITextBlock(new RectTransform(Vector2.One * 0.9f, enableModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, - TextManager.Get("ModsInstalledNotif"), Color.Black) - { - CanBeFocused = false - }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("EditorDisclaimerWikiLink"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, OnClicked = (button, userData) => { - string url = TextManager.Get("EditorDisclaimerWikiUrl", returnNull: true) ?? "https://barotraumagame.com/wiki"; + string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value; GameMain.Instance.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); return true; } }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("CreditsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.Credits, OnClicked = SelectTab }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("QuitButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, OnClicked = QuitClicked }; //debug button for quickly starting a new round #if DEBUG new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 80) }, - "Quickstart (dev)", style: "GUIButtonLarge", color: GUI.Style.Red) + "Quickstart (dev)", style: "GUIButtonLarge", color: GUIStyle.Red) { IgnoreLayoutGroups = true, UserData = Tab.Empty, @@ -387,7 +350,7 @@ namespace Barotrauma } }; new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 130) }, - "Profiling", style: "GUIButtonLarge", color: GUI.Style.Red) + "Profiling", style: "GUIButtonLarge", color: GUIStyle.Red) { IgnoreLayoutGroups = true, UserData = Tab.Empty, @@ -404,7 +367,7 @@ namespace Barotrauma } }; new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 180) }, - "Join Localhost", style: "GUIButtonLarge", color: GUI.Style.Red) + "Join Localhost", style: "GUIButtonLarge", color: GUIStyle.Red) { IgnoreLayoutGroups = true, UserData = Tab.Empty, @@ -413,7 +376,7 @@ namespace Barotrauma { SelectTab(tb, userdata); - GameMain.Client = new GameClient(string.IsNullOrEmpty(GameMain.Config.PlayerName) ? SteamManager.GetUsername() : GameMain.Config.PlayerName, + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), IPAddress.Loopback.ToString(), 0, "localhost", 0, false); return true; @@ -430,18 +393,19 @@ namespace Barotrauma var pivot = Pivot.CenterRight; Vector2 relativeSpacing = new Vector2(0.05f, 0.0f); - menuTabs = new GUIFrame[Enum.GetValues(typeof(Tab)).Length + 1]; + menuTabs = new Dictionary(); - menuTabs[(int)Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, + menuTabs[Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, style: null); + menuTabs[Tab.Settings].CanBeFocused = false; - menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); - menuTabs[(int)Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); CreateCampaignSetupUI(); var hostServerScale = new Vector2(0.7f, 1.2f); - menuTabs[(int)Tab.HostServer] = new GUIFrame(new RectTransform( + menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform( Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) { RelativeOffset = relativeSpacing }); @@ -449,36 +413,38 @@ namespace Barotrauma //---------------------------------------------------------------------- - menuTabs[(int)Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); //PLACEHOLDER var tutorialList = new GUIListBox( - new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[(int)Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); - foreach (Tutorial tutorial in Tutorial.Tutorials) + new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); + var tutorialTypes = ReflectionUtils.GetDerivedNonAbstract(); + foreach (Type tutorialType in tutorialTypes) { - var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUI.LargeFont) + Tutorial tutorial = (Tutorial)Activator.CreateInstance(tutorialType); + var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { UserData = tutorial }; } tutorialList.OnSelected += (component, obj) => { - TutorialMode.StartTutorial(obj as Tutorial); + (obj as Tutorial).Start(); return true; }; this.game = game; - menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) + menuTabs[Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { CanBeFocused = false }; - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[(int)Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker") + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker") { CanBeFocused = false }; - var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[(int)Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); + var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); } #endregion @@ -486,6 +452,14 @@ namespace Barotrauma #region Selection public override void Select() { + if (WorkshopItemsToUpdate.Any()) + { + while (WorkshopItemsToUpdate.TryDequeue(out ulong workshopId)) + { + SteamManager.Workshop.OnItemDownloadComplete(workshopId, forceInstall: true); + } + } + GUI.PreventPauseMenuToggle = false; base.Select(); @@ -500,30 +474,6 @@ namespace Barotrauma Submarine.Unload(); ResetButtonStates(null); - - if (GameMain.SteamWorkshopScreen != null) - { - CoroutineManager.StartCoroutine(GameMain.SteamWorkshopScreen.RefreshDownloadState()); - } - -#if OSX - // Hack for adjusting the viewport properly after splash screens on older Macs - if (firstLoadOnMac) - { - firstLoadOnMac = false; - - menuTabs[(int)Tab.Empty] = new GUIFrame(new RectTransform(new Vector2(1f, 1f), GUI.Canvas), "", Color.Transparent) - { - CanBeFocused = false - }; - var emptyList = new GUIListBox(new RectTransform(new Vector2(0.0f, 0.0f), menuTabs[(int)Tab.Empty].RectTransform)) - { - CanBeFocused = false - }; - - SelectTab(null, Tab.Empty); - } -#endif } public override void Deselect() @@ -549,44 +499,19 @@ namespace Barotrauma private bool SelectTab(Tab tab) { titleText.Visible = true; - if (GameMain.Config.UnsavedSettings) - { - var applyBox = new GUIMessageBox( - TextManager.Get("ApplySettingsLabel"), - TextManager.Get("ApplySettingsQuestion"), - new string[] { TextManager.Get("ApplySettingsYes"), TextManager.Get("ApplySettingsNo") }); - applyBox.Buttons[0].UserData = tab; - applyBox.Buttons[0].OnClicked = (tb, userdata) => - { - applyBox.Close(); - ApplySettings(); - SelectTab(tab); - return true; - }; - - applyBox.Buttons[1].UserData = tab; - applyBox.Buttons[1].OnClicked = (tb, userdata) => - { - applyBox.Close(); - DiscardSettings(); - SelectTab(tab); - return true; - }; - return false; - } - - GameMain.Config.ResetSettingsFrame(); + SettingsMenu.Instance?.Close(); + #warning TODO: reimplement settings confirmation dialog switch (tab) { case Tab.NewGame: - if (GameMain.Config.ShowTutorialSkipWarning) + if (GameSettings.CurrentConfig.TutorialSkipWarning) { selectedTab = Tab.Empty; ShowTutorialSkipWarning(Tab.NewGame); return true; } - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.NewGame); }); @@ -602,19 +527,16 @@ namespace Barotrauma campaignSetupUI.UpdateLoadMenu(); break; case Tab.Settings: - GameMain.MainMenuScreen?.SetEnableModsNotification(false); - menuTabs[(int)Tab.Settings].RectTransform.ClearChildren(); - GameMain.Config.SettingsFrame.RectTransform.Parent = menuTabs[(int)Tab.Settings].RectTransform; - GameMain.Config.SettingsFrame.RectTransform.RelativeSize = Vector2.One; + SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); break; case Tab.JoinServer: - if (GameMain.Config.ShowTutorialSkipWarning) + if (GameSettings.CurrentConfig.TutorialSkipWarning) { selectedTab = Tab.Empty; ShowTutorialSkipWarning(Tab.JoinServer); return true; } - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.JoinServer); }); @@ -623,19 +545,13 @@ namespace Barotrauma GameMain.ServerListScreen.Select(); break; case Tab.HostServer: - if (GameMain.Config.ContentPackageSelectionDirty) - { - new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("ServerRestartRequiredContentPackage", fallBackTag: "RestartRequiredGeneric")); - selectedTab = Tab.Empty; - return false; - } - if (GameMain.Config.ShowTutorialSkipWarning) + if (GameSettings.CurrentConfig.TutorialSkipWarning) { selectedTab = Tab.Empty; ShowTutorialSkipWarning(tab); return true; } - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.HostServer); }); @@ -643,7 +559,7 @@ namespace Barotrauma } break; case Tab.Tutorials: - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.Tutorials); }); @@ -659,8 +575,9 @@ namespace Barotrauma CoroutineManager.StartCoroutine(SelectScreenWithWaitCursor(GameMain.SubEditorScreen)); break; case Tab.SteamWorkshop: - if (!Steam.SteamManager.IsInitialized) return false; - CoroutineManager.StartCoroutine(SelectScreenWithWaitCursor(GameMain.SteamWorkshopScreen)); + var settings = SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); + settings.SelectTab(SettingsMenu.Tab.Mods); + tab = Tab.Settings; break; case Tab.Credits: titleText.Visible = false; @@ -717,7 +634,7 @@ namespace Barotrauma } #endregion - public void QuickStart(bool fixedSeed = false, string sub = null, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) + public void QuickStart(bool fixedSeed = false, Identifier sub = default, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) { if (fixedSeed) { @@ -726,11 +643,12 @@ namespace Barotrauma } SubmarineInfo selectedSub = null; - string subName = sub ?? GameMain.Config.QuickStartSubmarineName; - if (!string.IsNullOrEmpty(subName)) + Identifier subName = sub.IfEmpty(GameSettings.CurrentConfig.QuickStartSub); + if (!subName.IsEmpty) { DebugConsole.NewMessage($"Loading the predefined quick start sub \"{subName}\"", Color.White); - selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLowerInvariant() == subName.ToLowerInvariant()); + + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); if (selectedSub == null) { DebugConsole.NewMessage($"Cannot find a sub that matches the name \"{subName}\".", Color.Red); @@ -755,7 +673,7 @@ namespace Barotrauma { var jobPrefab = JobPrefab.Get(job); var variant = Rand.Range(0, jobPrefab.Variants); - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant); if (characterInfo.Job == null) { DebugConsole.ThrowError("Failed to find the job \"" + job + "\"!"); @@ -765,40 +683,29 @@ namespace Barotrauma gamesession.CrewManager.InitSinglePlayerRound(); } - public void SetEnableModsNotification(bool visible) - { - if (enableModsContainer != null) { enableModsContainer.Visible = visible; } - } - - public void SetDownloadingModsNotification(bool visible) - { - if (downloadingModsContainer != null) { downloadingModsContainer.Visible = visible; } - } - private void ShowTutorialSkipWarning(Tab tabToContinueTo) { - var tutorialSkipWarning = new GUIMessageBox("", TextManager.Get("tutorialskipwarning"), new string[] { TextManager.Get("tutorialwarningskiptutorials"), TextManager.Get("tutorialwarningplaytutorials") }); - tutorialSkipWarning.Buttons[0].OnClicked += (btn, userdata) => - { - GameMain.Config.ShowTutorialSkipWarning = false; - GameMain.Config.SaveNewPlayerConfig(); - tutorialSkipWarning.Close(); - SelectTab(tabToContinueTo); - return true; - }; - tutorialSkipWarning.Buttons[1].OnClicked += (btn, userdata) => - { - GameMain.Config.ShowTutorialSkipWarning = false; - GameMain.Config.SaveNewPlayerConfig(); - tutorialSkipWarning.Close(); - SelectTab(Tab.Tutorials); - return true; - }; + var tutorialSkipWarning = new GUIMessageBox("", TextManager.Get("tutorialskipwarning"), new LocalizedString[] { TextManager.Get("tutorialwarningskiptutorials"), TextManager.Get("tutorialwarningplaytutorials") }); + + GUIButton.OnClickedHandler proceedToTab(Tab tab) + => (btn, userdata) => + { + var config = GameSettings.CurrentConfig; + config.TutorialSkipWarning = false; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); + tutorialSkipWarning.Close(); + SelectTab(tab); + return true; + }; + + tutorialSkipWarning.Buttons[0].OnClicked += proceedToTab(tabToContinueTo); + tutorialSkipWarning.Buttons[1].OnClicked += proceedToTab(Tab.Tutorials); } private void UpdateTutorialList() { - var tutorialList = menuTabs[(int)Tab.Tutorials].GetChild(); + var tutorialList = menuTabs[Tab.Tutorials].GetChild(); int completedTutorials = 0; @@ -814,7 +721,7 @@ namespace Barotrauma { if (i < completedTutorials + 1) { - (tutorialList.Content.GetChild(i) as GUITextBlock).TextColor = GUI.Style.Green; + (tutorialList.Content.GetChild(i) as GUITextBlock).TextColor = GUIStyle.Green; #if !DEBUG (tutorialList.Content.GetChild(i) as GUITextBlock).CanBeFocused = true; #endif @@ -829,37 +736,6 @@ namespace Barotrauma } } - public void ResetSettingsFrame(GameSettings.Tab selectedTab = GameSettings.Tab.Graphics) - { - menuTabs[(int)Tab.Settings].RectTransform.ClearChildren(); - GameMain.Config.ResetSettingsFrame(); - GameMain.Config.CreateSettingsFrame(selectedTab); - GameMain.Config.SettingsFrame.RectTransform.Parent = menuTabs[(int)Tab.Settings].RectTransform; - GameMain.Config.SettingsFrame.RectTransform.RelativeSize = Vector2.One; - } - - private bool ApplySettings() - { - GameMain.Config.SaveNewPlayerConfig(); - - if (GameMain.GraphicsWidth != GameMain.Config.GraphicsWidth || - GameMain.GraphicsHeight != GameMain.Config.GraphicsHeight) - { - new GUIMessageBox( - TextManager.Get("RestartRequiredLabel"), - TextManager.Get("RestartRequiredGeneric")); - } - - return true; - } - - private bool DiscardSettings() - { - GameMain.Config.LoadPlayerConfig(); - - return true; - } - private bool ChangeMaxPlayers(GUIButton button, object obj) { int.TryParse(maxPlayersBox.Text, out int currMaxPlayers); @@ -872,7 +748,7 @@ namespace Barotrauma { if (SubmarineInfo.SavedSubmarines.Any(s => s.CalculatingHash)) { - var waitBox = new GUIMessageBox(TextManager.Get("pleasewait"), TextManager.Get("waitforsubmarinehashcalculations"), new string[] { TextManager.Get("cancel") }); + var waitBox = new GUIMessageBox(TextManager.Get("pleasewait"), TextManager.Get("waitforsubmarinehashcalculations"), new LocalizedString[] { TextManager.Get("cancel") }); var waitCoroutine = CoroutineManager.StartCoroutine(WaitForSubmarineHashCalculations(waitBox), "WaitForSubmarineHashCalculations"); waitBox.Buttons[0].OnClicked += (btn, userdata) => { @@ -888,7 +764,7 @@ namespace Barotrauma private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) { - string originalText = messageBox.Text.Text; + LocalizedString originalText = messageBox.Text.Text; int doneCount = 0; do { @@ -905,16 +781,10 @@ namespace Barotrauma { string name = serverNameBox.Text; - GameMain.NetLobbyScreen?.Release(); - GameMain.NetLobbyScreen = new NetLobbyScreen(); + GameMain.ResetNetLobbyScreen(); try { - string exeName = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.ServerExecutable)?.FirstOrDefault()?.Path; - if (string.IsNullOrEmpty(exeName)) - { - DebugConsole.ThrowError("No server executable defined in the selected content packages. Attempting to use the default executable..."); - exeName = "DedicatedServer.exe"; - } + string exeName = "DedicatedServer.exe"; string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -962,7 +832,8 @@ namespace Barotrauma ChildServerRelay.Start(processInfo); Thread.Sleep(1000); //wait until the server is ready before connecting - GameMain.Client = new GameClient(string.IsNullOrEmpty(GameMain.Config.PlayerName) ? name : GameMain.Config.PlayerName, + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty( + SteamManager.GetUsername().FallbackNullOrEmpty(name)), System.Net.IPAddress.Loopback.ToString(), Steam.SteamManager.GetSteamID(), name, ownerKey, true); } catch (Exception e) @@ -980,9 +851,9 @@ namespace Barotrauma public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - if (selectedTab < Tab.Empty && menuTabs[(int)selectedTab] != null) + if (selectedTab < Tab.Empty && menuTabs.TryGetValue(selectedTab, out GUIFrame tab) && tab != null) { - menuTabs[(int)selectedTab].AddToGUIUpdateList(); + tab.AddToGUIUpdateList(); switch (selectedTab) { case Tab.NewGame: @@ -995,9 +866,9 @@ namespace Barotrauma public override void Update(double deltaTime) { #if !DEBUG && USE_STEAM - if (GameMain.Config.UseSteamMatchmaking) + if (GameSettings.CurrentConfig.UseSteamMatchmaking) { - hostServerButton.Enabled = Steam.SteamManager.IsInitialized; + hostServerButton.Enabled = Steam.SteamManager.IsInitialized; } steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; #elif USE_STEAM @@ -1020,7 +891,7 @@ namespace Barotrauma #if UNSTABLE backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); #endif - backgroundSprite ??= LocationType.List.Where(l => l.UseInMainMenu).GetRandom()?.GetPortrait(0); + backgroundSprite ??= (LocationType.Prefabs.Where(l => l.UseInMainMenu).GetRandomUnsynced())?.GetPortrait(0); } if (backgroundSprite != null) @@ -1029,7 +900,7 @@ namespace Barotrauma aberrationStrength: 0.0f); } - var vignette = GUI.Style.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); + var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); if (vignette != null) { spriteBatch.Begin(blendState: BlendState.NonPremultiplied); @@ -1039,9 +910,9 @@ namespace Barotrauma } } - readonly string[] legalCrap = new string[] + readonly LocalizedString[] legalCrap = new LocalizedString[] { - TextManager.Get("privacypolicy", returnNull: true) ?? "Privacy policy", + TextManager.Get("privacypolicy").Fallback("Privacy policy"), "© " + DateTime.Now.Year + " Undertow Games & FakeFish. All rights reserved.", "© " + DateTime.Now.Year + " Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved." }; @@ -1054,28 +925,27 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); - if (selectedTab != Tab.Credits) { #if !UNSTABLE string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; - GUI.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); + GUIStyle.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUIStyle.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); #endif - string gameAnalyticsStatus = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); - Vector2 textSize = GUI.SmallFont.MeasureString(gameAnalyticsStatus).ToPoint().ToVector2(); - GUI.SmallFont.DrawString(spriteBatch, gameAnalyticsStatus, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.LineHeight * 2 - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); + LocalizedString gameAnalyticsStatus = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(gameAnalyticsStatus).ToPoint().ToVector2(); + GUIStyle.SmallFont.DrawString(spriteBatch, gameAnalyticsStatus, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUIStyle.SmallFont.LineHeight * 2 - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); Vector2 textPos = new Vector2(GameMain.GraphicsWidth - HUDLayoutSettings.Padding, GameMain.GraphicsHeight - HUDLayoutSettings.Padding * 0.75f); for (int i = legalCrap.Length - 1; i >= 0; i--) { - textSize = GUI.SmallFont.MeasureString(legalCrap[i]) + textSize = GUIStyle.SmallFont.MeasureString(legalCrap[i]) .ToPoint().ToVector2(); bool mouseOn = i == 0 && - PlayerInput.MousePosition.X > textPos.X - textSize.X && PlayerInput.MousePosition.X < textPos.X && - PlayerInput.MousePosition.Y > textPos.Y - textSize.Y && PlayerInput.MousePosition.Y < textPos.Y; + PlayerInput.MousePosition.X > textPos.X - textSize.X && PlayerInput.MousePosition.X < textPos.X && + PlayerInput.MousePosition.Y > textPos.Y - textSize.Y && PlayerInput.MousePosition.Y < textPos.Y; - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, legalCrap[i], textPos - textSize, mouseOn ? Color.White : Color.White * 0.7f); @@ -1163,10 +1033,10 @@ namespace Barotrauma #region UI Methods private void CreateCampaignSetupUI() { - menuTabs[(int)Tab.NewGame].ClearChildren(); - menuTabs[(int)Tab.LoadGame].ClearChildren(); + menuTabs[Tab.NewGame].ClearChildren(); + menuTabs[Tab.LoadGame].ClearChildren(); - var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center)) + var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[Tab.NewGame].RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f @@ -1174,7 +1044,7 @@ namespace Barotrauma var newGameContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.95f), innerNewGame.RectTransform, Anchor.Center), style: "InnerFrame"); - var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, + var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame, SubmarineInfo.SavedSubmarines) @@ -1186,7 +1056,7 @@ namespace Barotrauma private void CreateHostServerFields() { - menuTabs[(int)Tab.HostServer].ClearChildren(); + menuTabs[Tab.HostServer].ClearChildren(); string name = ""; string password = ""; @@ -1226,14 +1096,14 @@ namespace Barotrauma Alignment textAlignment = Alignment.CenterLeft; Vector2 textFieldSize = new Vector2(0.5f, 1.0f); Vector2 tickBoxSize = new Vector2(0.4f, 0.07f); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[(int)Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f, Stretch = true }; GUIComponent parent = content; - new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Center, font: GUI.LargeFont) { ForceUpperCase = true }; + new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { ForceUpperCase = ForceUpperCase.Yes }; //play style ----------------------------------------------------- @@ -1250,7 +1120,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(Vector2.One, playstyleBanner.RectTransform), "InnerGlow", color: Color.Black); new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), playstyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.03f) }, - "playstyle name goes here", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); + "playstyle name goes here", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), playstyleContainer.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, @@ -1278,10 +1148,10 @@ namespace Barotrauma } }; - string longestPlayStyleStr = ""; + LocalizedString longestPlayStyleStr = ""; foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { - string playStyleStr = TextManager.Get("servertagdescription." + playStyle); + LocalizedString playStyleStr = TextManager.Get("servertagdescription." + playStyle); if (playStyleStr.Length > longestPlayStyleStr.Length) { longestPlayStyleStr = playStyleStr; } } @@ -1289,7 +1159,7 @@ namespace Barotrauma longestPlayStyleStr, style: null, wrap: true) { Color = Color.Black * 0.8f, - TextColor = GUI.Style.GetComponentStyle("GUITextBlock").TextColor + TextColor = GUIStyle.GetComponentStyle("GUITextBlock").TextColor }; playstyleDescription.Padding = Vector4.One * 10.0f * GUI.Scale; playstyleDescription.CalculateHeightFromText(padding: (int)(15 * GUI.Scale)); @@ -1399,8 +1269,8 @@ namespace Barotrauma if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) { var msgBox = new GUIMessageBox("", - TextManager.GetWithVariables("forbiddenservernameverification", new string[] { "[forbiddenword]", "[servername]" }, new string[] { forbiddenWord, name }), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + TextManager.GetWithVariables("forbiddenservernameverification", ("[forbiddenword]", forbiddenWord), ("[servername]", name)), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked += (_, __) => { TryStartServer(); @@ -1437,10 +1307,10 @@ namespace Barotrauma private void FetchRemoteContent() { - if (string.IsNullOrEmpty(GameMain.Config.RemoteContentUrl)) { return; } + if (string.IsNullOrEmpty(RemoteContentUrl)) { return; } try { - var client = new RestClient(GameMain.Config.RemoteContentUrl); + var client = new RestClient(RemoteContentUrl); var request = new RestRequest("MenuContent.xml", Method.GET); client.ExecuteAsync(request, RemoteContentReceived); CoroutineManager.StartCoroutine(WairForRemoteContentReceived()); @@ -1482,9 +1352,9 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(xml)) { remoteContentDoc = XDocument.Parse(xml); - foreach (XElement subElement in remoteContentDoc?.Root.Elements()) + foreach (var subElement in remoteContentDoc?.Root.Elements()) { - GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs new file mode 100644 index 000000000..478571b01 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -0,0 +1,296 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.IO; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Steamworks.Data; +using Color = Microsoft.Xna.Framework.Color; +using ServerContentPackage = Barotrauma.Networking.ClientPeer.ServerContentPackage; + +namespace Barotrauma +{ + class ModDownloadScreen : Screen + { + private readonly Queue pendingDownloads = + new Queue(); + private ServerContentPackage? currentDownload; + + private readonly List downloadedPackages = new List(); + + private bool confirmDownload; + + private void Reset() + { + pendingDownloads.Clear(); + downloadedPackages.Clear(); + currentDownload = null; + confirmDownload = false; + } + + public override void Select() + { + base.Select(); + Reset(); + + Frame.ClearChildren(); + + var mainVisibleFrame = new GUIFrame(new RectTransform((0.6f, 0.8f), Frame.RectTransform, Anchor.Center)); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.93f, mainVisibleFrame.RectTransform, Anchor.Center)); + + void mainLayoutSpacing() + => new GUIFrame(new RectTransform((1.0f, 0.02f), mainLayout.RectTransform), style: null); + + var serverName = new GUITextBlock(new RectTransform((1.0f, 0.08f), mainLayout.RectTransform), + "", font: GUIStyle.LargeFont, + textAlignment: Alignment.CenterLeft) + { + TextGetter = () => GameMain.NetLobbyScreen.ServerName.Text + }; + mainLayoutSpacing(); + var downloadList = new GUIListBox(new RectTransform((1.0f, 0.76f), mainLayout.RectTransform)); + mainLayoutSpacing(); + var disconnectButton = new GUIButton(new RectTransform((0.3f, 0.1f), mainLayout.RectTransform), + TextManager.Get("Disconnect")) + { + OnClicked = (guiButton, o) => + { + GameMain.Client.Disconnect(); + GameMain.MainMenuScreen.Select(); + return false; + } + }; + + var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages + .Where(sp => sp.ContentPackage is null).ToArray(); + if (!missingPackages.Any()) + { + GameMain.NetLobbyScreen.Select(); + return; + } + + GUIMessageBox msgBox = new GUIMessageBox( + TextManager.Get("WorkshopItemDownloadTitle"), + "", + Array.Empty(), + relativeSize: (0.5f, 0.75f)); + + GUILayoutGroup innerLayout = msgBox.Content; + innerLayout.Stretch = true; + + void innerLayoutSpacing(float height) + => new GUIFrame(new RectTransform((1.0f, height), innerLayout.RectTransform), style: null); + + GUITextBlock textBlock(LocalizedString str, GUIFont font, Alignment alignment = Alignment.CenterLeft) + { + var tb = new GUITextBlock(new RectTransform(Point.Zero, innerLayout.RectTransform), str, + wrap: true, textAlignment: alignment, font: font); + new GUICustomComponent(new RectTransform(Vector2.Zero, tb.RectTransform), onUpdate: + (deltaTime, component) => + { + if (tb.RectTransform.NonScaledSize.X != innerLayout.Rect.Width) + { + tb.RectTransform.NonScaledSize = (innerLayout.Rect.Width, 0); + tb.RectTransform.NonScaledSize = (innerLayout.Rect.Width, + (int)tb.Font.MeasureString(tb.WrappedText).Y); + } + }); + return tb; + } + + var title = textBlock(TextManager.Get("ModDownloadTitle"), GUIStyle.SubHeadingFont, Alignment.Center); + innerLayoutSpacing(0.05f); + var header = textBlock(TextManager.Get("ModDownloadHeader"), GUIStyle.Font); + innerLayoutSpacing(0.05f); + + var msgBoxModList = new GUIListBox(new RectTransform(Vector2.One, innerLayout.RectTransform)); + + innerLayoutSpacing(0.05f); + var footer = textBlock(TextManager.Get("ModDownloadFooter"), GUIStyle.Font, Alignment.Center); + + innerLayoutSpacing(0.05f); + GUILayoutGroup buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true); + + void buttonContainerSpacing(float width) + => new GUIFrame(new RectTransform((width, 1.0f), buttonContainer.RectTransform), style: null); + + void button(LocalizedString text, Action action) + => new GUIButton(new RectTransform((0.3f, 1.0f), buttonContainer.RectTransform), text) + { + OnClicked = (_, __) => + { + action(); + msgBox.Close(); + return false; + } + }; + + buttonContainerSpacing(0.1f); + button(TextManager.Get("Yes"), () => confirmDownload = true); + buttonContainerSpacing(0.2f); + button(TextManager.Get("No"), () => + { + GameMain.Client.Disconnect(); + GameMain.MainMenuScreen.Select(); + }); + buttonContainerSpacing(0.1f); + + foreach (var p in missingPackages) + { + pendingDownloads.Enqueue(p); + + //Message box frame + new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBoxModList.Content.RectTransform), p.Name) + { + CanBeFocused = false + }; + + //Download progress frame + var downloadFrame = new GUIFrame(new RectTransform((1.0f, 0.06f), downloadList.Content.RectTransform), + style: "ListBoxElement") + { + UserData = p, + CanBeFocused = false + }; + new GUITextBlock(new RectTransform((0.5f, 1.0f), downloadFrame.RectTransform), p.Name) + { + CanBeFocused = false + }; + var downloadProgress = new GUIProgressBar( + new RectTransform((0.5f, 0.75f), downloadFrame.RectTransform, Anchor.CenterRight), + 0.0f, color: GUIStyle.Green); + downloadProgress.ProgressGetter = () => + { + if (currentDownload == p) + { + FileReceiver.FileTransferIn? getTransfer() => GameMain.Client.FileReceiver.ActiveTransfers.FirstOrDefault(t => t.FileType == FileTransferType.Mod); + + if (downloadProgress.GetAnyChild() is null) + { + GUILayoutGroup progressBarLayout + = new GUILayoutGroup(new RectTransform(Vector2.One, downloadProgress.RectTransform), isHorizontal: true); + + void progressBarText(float width, Alignment textAlignment, Func getter) + { + var textContainer = new GUIFrame(new RectTransform((width, 1.0f), progressBarLayout.RectTransform), + style: null); + var textShadow = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, "", + textColor: Color.Black, textAlignment: textAlignment); + var text = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), "", + textAlignment: textAlignment); + new GUICustomComponent(new RectTransform(Vector2.Zero, textContainer.RectTransform), onUpdate: + (f, component) => + { + string str = getter(); + if (text.Text?.SanitizedValue != str) + { + text.Text = str; + textShadow.Text = str; + } + }); + } + progressBarText(0.475f, Alignment.CenterRight, () => MathUtils.GetBytesReadable(getTransfer()?.Received ?? 0)); + progressBarText(0.05f, Alignment.Center, () => "/"); + progressBarText(0.475f, Alignment.CenterLeft, () => MathUtils.GetBytesReadable(getTransfer()?.FileSize ?? 0)); + } + + return getTransfer()?.Progress ?? 0.0f; + } + + if (!pendingDownloads.Contains(p)) + { + downloadProgress.ClearChildren(); + return 1.0f; + } + + return 0.0f; + }; + } + } + + public override void Deselect() + { + Reset(); + base.Deselect(); + } + + public override void Update(double deltaTime) + { + base.Update(deltaTime); + if (GameMain.Client is null) { return; } + if (!confirmDownload) { return; } + if (currentDownload is null) + { + if (pendingDownloads.TryDequeue(out currentDownload)) + { + GameMain.Client.RequestFile(FileTransferType.Mod, currentDownload.Name, currentDownload.Hash.StringRepresentation); + } + else + { + var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages; + CorePackage corePackage + = downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage + ?? serverPackages.FirstOrDefault(p => p.CorePackage != null) + ?.CorePackage + ?? throw new Exception($"Failed to find core package to enable"); + RegularPackage[] regularPackages + = serverPackages.Where(p => p.CorePackage is null) + .Select(p => + p.RegularPackage + ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) + ?? throw new Exception($"Could not find regular package \"{p.Name}\"")) + .Cast() + .ToArray(); + foreach (var regularPackage in regularPackages) + { + DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime); + } + + ContentPackageManager.EnabledPackages.BackUp(); + ContentPackageManager.EnabledPackages.SetCore(corePackage); + ContentPackageManager.EnabledPackages.SetRegular(regularPackages); + + GameMain.NetLobbyScreen.Select(); + } + } + } + + public void CurrentDownloadFinished(FileReceiver.FileTransferIn transfer) + { + if (currentDownload is null) { throw new Exception("Current download is null"); } + + string path = transfer.FilePath; + if (!path.EndsWith(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase)) + { + return; + } + string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); + + SaveUtil.DecompressToDirectory(path, dir, file => { }); + ContentPackage newPackage + = ContentPackage.TryLoad($"{dir}/{ContentPackage.FileListFileName}") + ?? throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\""); + if (!currentDownload.Hash.Equals(newPackage.Hash)) + { + throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})"); + } + downloadedPackages.Add(newPackage); + + currentDownload = null; + + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); //wtf + + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + + GUI.Draw(Cam, spriteBatch); + + spriteBatch.End(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index da26a5575..e035855de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -264,7 +264,7 @@ namespace Barotrauma } } - public List> JobPreferences + public List JobPreferences { get { @@ -272,13 +272,13 @@ namespace Barotrauma // (e.g. the player has a pre-existing campaign character) if (JobList?.Content == null) { - return new List>(); + return new List(); } - List> jobPreferences = new List>(); + List jobPreferences = new List(); foreach (GUIComponent child in JobList.Content.Children) { - if (!(child.UserData is Pair jobPrefab)) { continue; } + if (!(child.UserData is JobVariant jobPrefab)) { continue; } jobPreferences.Add(jobPrefab); } return jobPreferences; @@ -384,14 +384,14 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.05f }; - FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUI.SmallFont); + FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont); var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", - font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => @@ -527,7 +527,7 @@ namespace Barotrauma chatInput = new GUITextBox(new RectTransform(new Vector2(0.95f, 1.0f), chatRow.RectTransform)) { MaxTextLength = ChatMessage.MaxLength, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, DeselectAfterMessage = false }; @@ -578,7 +578,7 @@ namespace Barotrauma serverLogFilter = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.07f), serverLogHolder.RectTransform)) { MaxTextLength = ChatMessage.MaxLength, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), @@ -618,7 +618,7 @@ namespace Barotrauma //autorestart ------------------------------------------------------------------ - autoRestartText = new GUITextBlock(new RectTransform(Vector2.One, bottomBarMid.RectTransform), "", font: GUI.SmallFont, style: "TextFrame", textAlignment: Alignment.Center); + autoRestartText = new GUITextBlock(new RectTransform(Vector2.One, bottomBarMid.RectTransform), "", font: GUIStyle.SmallFont, style: "TextFrame", textAlignment: Alignment.Center); GUIFrame autoRestartBoxContainer = new GUIFrame(new RectTransform(Vector2.One, bottomBarMid.RectTransform), style: "TextFrame"); autoRestartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), autoRestartBoxContainer.RectTransform, Anchor.Center), TextManager.Get("AutoRestart")) { @@ -704,13 +704,13 @@ namespace Barotrauma HideElementsOutsideFrame = true }; new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), serverBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.04f) }, - "", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") { CanBeFocused = false }; publicOrPrivate = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), serverBanner.RectTransform, Anchor.BottomRight, Pivot.BottomRight), - "", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") { CanBeFocused = false }; @@ -719,7 +719,7 @@ namespace Barotrauma ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft); var serverMessageHint = new GUITextBlock(new RectTransform(Vector2.One, ServerMessage.RectTransform), - textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUI.Style.Font, text: TextManager.Get("ClickToWriteServerMessage")); + textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, text: TextManager.Get("ClickToWriteServerMessage")); void updateServerMessageScrollBasedOnCaret() { @@ -786,7 +786,7 @@ namespace Barotrauma Stretch = true }; - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUI.SubHeadingFont); + var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUIStyle.SubHeadingFont); SubVisibilityButton = new GUIButton( @@ -806,8 +806,8 @@ namespace Barotrauma { Stretch = true }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - subSearchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); + subSearchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; @@ -895,7 +895,7 @@ namespace Barotrauma Stretch = true }; - var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), gameModeHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("GameMode"), font: GUI.SubHeadingFont); + var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), gameModeHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("GameMode"), font: GUIStyle.SubHeadingFont); voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) { @@ -922,8 +922,8 @@ namespace Barotrauma Stretch = true }; - var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUI.SubHeadingFont); - var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUI.SmallFont, wrap: true); + var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); + var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; @@ -953,7 +953,7 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("gamemode.multiplayercampaign"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center); + TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); ContinueCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), TextManager.Get("campaigncontinue"), textAlignment: Alignment.Center) { @@ -981,9 +981,9 @@ namespace Barotrauma { Stretch = true }; - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("MissionType"), font: GUI.SubHeadingFont); + TextManager.Get("MissionType"), font: GUIStyle.SubHeadingFont); missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) { OnSelected = (component, obj) => @@ -1020,7 +1020,7 @@ namespace Barotrauma TextManager.Get("MissionType." + missionType.ToString())) { UserData = (int)missionType, - ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString(), returnNull: true), + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), OnSelected = (tickbox) => { int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; @@ -1045,7 +1045,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("Settings"), font: GUI.SubHeadingFont); + TextManager.Get("Settings"), font: GUIStyle.SubHeadingFont); var settingsFrame = new GUIFrame(new RectTransform(Vector2.One, settingsHolder.RectTransform), style: "InnerFrame"); var settingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsFrame.RectTransform, Anchor.Center)) { @@ -1089,11 +1089,11 @@ namespace Barotrauma }; levelDifficultyScrollBar.OnMoved = (scrollbar, value) => { - if (EventManagerSettings.List.Count == 0) { return true; } + if (!EventManagerSettings.Prefabs.Any()) { return true; } difficultyName.Text = - EventManagerSettings.List[Math.Min((int)Math.Floor(value * EventManagerSettings.List.Count), EventManagerSettings.List.Count - 1)].Name + EventManagerSettings.GetByDifficultyPercentile(value).Name + " (" + ((int)Math.Round(scrollbar.BarScrollValue)) + " %)"; - difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return true; }; @@ -1214,8 +1214,8 @@ namespace Barotrauma public IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); - string headerText = TextManager.Get("RoundStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), new string[0]); + LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait"); + var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), Array.Empty()); if (startButton != null) { @@ -1405,7 +1405,7 @@ namespace Barotrauma if (characterInfo == null || CampaignCharacterDiscarded) { characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); - characterInfo.RecreateHead(GameMain.Config.PlayerCharacterCustomization); + characterInfo.RecreateHead(MultiplayerPreferences.Instance); GameMain.Client.CharacterInfo = characterInfo; characterInfo.OmitJobInPortraitClothing = false; } @@ -1517,20 +1517,20 @@ namespace Barotrauma for (int i = 0; i < 3; i++) { - Pair jobPrefab = null; - while (i < GameMain.Config.JobPreferences.Count) + JobVariant jobPrefab = null; + while (i < MultiplayerPreferences.Instance.JobPreferences.Count) { - var jobIdentifier = GameMain.Config.JobPreferences[i]; - if (!JobPrefab.Prefabs.ContainsKey(jobIdentifier.First)) + var jobPreference = MultiplayerPreferences.Instance.JobPreferences[i]; + if (!JobPrefab.Prefabs.ContainsKey(jobPreference.JobIdentifier)) { - GameMain.Config.JobPreferences.RemoveAt(i); + MultiplayerPreferences.Instance.JobPreferences.RemoveAt(i); continue; } // The old job variant system used one-based indexing // so let's make sure no one get to pick a variant which doesn't exist - var prefab = JobPrefab.Prefabs[jobIdentifier.First]; - var variant = Math.Min(jobIdentifier.Second, prefab.Variants - 1); - jobPrefab = new Pair(prefab, variant); + var prefab = JobPrefab.Prefabs[jobPreference.JobIdentifier]; + var variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); + jobPrefab = new JobVariant(prefab, variant); break; } @@ -1553,20 +1553,20 @@ namespace Barotrauma { characterInfo.CreateIcon(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUI.SubHeadingFont, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, SelectedColor = Color.Transparent }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); foreach (Skill skill in characterInfo.Job.Skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), textColor, - font: GUI.SmallFont); + font: GUIStyle.SmallFont); } // Spacing @@ -1644,15 +1644,15 @@ namespace Barotrauma SelectedTextColor = Color.White }; - TeamPreferenceListBox.Select(GameMain.Config.TeamPreference); + TeamPreferenceListBox.Select(MultiplayerPreferences.Instance.TeamPreference); TeamPreferenceListBox.OnSelected += (component, obj) => { - if ((CharacterTeamType)obj == GameMain.Config.TeamPreference) { return true; } + if ((CharacterTeamType)obj == MultiplayerPreferences.Instance.TeamPreference) { return true; } - GameMain.Config.TeamPreference = (CharacterTeamType)obj; + MultiplayerPreferences.Instance.TeamPreference = (CharacterTeamType)obj; GameMain.Client.ForceNameAndJobUpdate(); - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); return true; }; @@ -1685,7 +1685,7 @@ namespace Barotrauma IgnoreLayoutGroups = true }; var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center), - TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null); + TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null); changesPendingText.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.2f), (int)(text.TextSize.Y * 2.0f)); } @@ -1698,7 +1698,7 @@ namespace Barotrauma Color = Color.Black }; new GUITextBlock(new RectTransform(Vector2.One, changesPendingFrame.RectTransform, Anchor.Center), - TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null) + TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null) { AutoScaleHorizontal = true }; @@ -1709,7 +1709,7 @@ namespace Barotrauma jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), style: "GUIToolTip") { - UserData = new Pair(jobPrefab, variant) + UserData = new JobVariant(jobPrefab, variant) }; jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.Right, parentSlot.Rect.Y); @@ -1718,8 +1718,8 @@ namespace Barotrauma Stretch = true, AbsoluteSpacing = (int)(15 * GUI.Scale) }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUI.SubHeadingFont, textAlignment: Alignment.Center); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); var itemIdentifiers = jobPrefab.PreviewItems[variant] .Where(it => it.ShowPreview) @@ -1730,7 +1730,7 @@ namespace Barotrauma int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1); new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.4f * rows), content.RectTransform, Anchor.BottomCenter), - onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new Pair(jobPrefab, variant), itemsPerRow); }); + onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new JobVariant(jobPrefab, variant), itemsPerRow); }); jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing)); } @@ -1818,12 +1818,12 @@ namespace Barotrauma int buttonSize = (int)(frame.Rect.Height * 0.8f); var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft) /*{ AbsoluteOffset = new Point(buttonSize + 5, 0) }*/, - ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) + ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) { CanBeFocused = false }; - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation); if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); if (matchingSub == null) @@ -1831,7 +1831,7 @@ namespace Barotrauma subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); frame.ToolTip = TextManager.Get("SubNotFound"); } - else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.Hash != sub.MD5Hash?.Hash) + else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.StringRepresentation != sub.MD5Hash?.StringRepresentation) { subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); frame.ToolTip = TextManager.Get("SubDoesntMatch"); @@ -1847,7 +1847,7 @@ namespace Barotrauma if (!sub.RequiredContentPackagesInstalled) { subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); - frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.RawToolTip; + frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.ToolTip.SanitizedString; } CreateSubmarineClassText( @@ -1866,10 +1866,10 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.Shuttle)) { var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, - TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, - ToolTip = subTextBlock.RawToolTip, + ToolTip = subTextBlock.ToolTip?.SanitizedString, CanBeFocused = false }; //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) @@ -1885,11 +1885,11 @@ namespace Barotrauma else { var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, - ToolTip = subTextBlock.RawToolTip, + ToolTip = subTextBlock.ToolTip, CanBeFocused = false }; } @@ -1911,7 +1911,7 @@ namespace Barotrauma selectedSub.RequiredContentPackages.Any() ? TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)) : TextManager.Get("ContentPackageMismatchWarningGeneric"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => @@ -1941,14 +1941,14 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.SelectMode)) { - string presetName = ((GameModePreset)component.UserData).Identifier; + Identifier presetName = ((GameModePreset)component.UserData).Identifier; //display a verification prompt when switching away from the campaign if (HighlightedModeIndex == SelectedModeIndex && (GameMain.NetLobbyScreen.ModeList.SelectedData as GameModePreset) == GameModePreset.MultiPlayerCampaign && presetName != GameModePreset.MultiPlayerCampaign.Identifier) { - var verificationBox = new GUIMessageBox("", TextManager.Get("endcampaignverification"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var verificationBox = new GUIMessageBox("", TextManager.Get("endcampaignverification"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); verificationBox.Buttons[0].OnClicked += (btn, userdata) => { GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); @@ -1962,7 +1962,7 @@ namespace Barotrauma GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); HighlightMode(SelectedModeIndex); - if (presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) + if (presetName == "multiplayercampaign") { GUI.SetCursorWaiting(endCondition: () => { @@ -1970,7 +1970,7 @@ namespace Barotrauma }); } - return !presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase); + return presetName != "multiplayercampaign"; } return false; } @@ -1994,7 +1994,7 @@ namespace Barotrauma public void AddPlayer(Client client) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), PlayerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, - client.Name, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, style: null) + client.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, style: null) { Padding = Vector4.One * 10.0f * GUI.Scale, Color = Color.White * 0.25f, @@ -2006,7 +2006,7 @@ namespace Barotrauma UserData = client }; var soundIcon = new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, - sprite: GUI.Style.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) + sprite: GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { UserData = new Pair("soundicon", 0.0f), CanBeFocused = false, @@ -2170,7 +2170,7 @@ namespace Barotrauma { permissionOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => { - string label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, rank.Name }); + LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.Name)); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = delegate @@ -2257,7 +2257,7 @@ namespace Barotrauma }; var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), - text: selectedClient.Name, font: GUI.LargeFont); + text: selectedClient.Name, font: GUIStyle.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); if (hasManagePermissions) @@ -2265,7 +2265,7 @@ namespace Barotrauma PlayerFrame.UserData = selectedClient; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), - TextManager.Get("Rank"), font: GUI.SubHeadingFont); + TextManager.Get("Rank"), font: GUIStyle.SubHeadingFont); var rankDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank")) { @@ -2303,9 +2303,9 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.05f }; - var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUI.SubHeadingFont); + var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUIStyle.SubHeadingFont); var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), - TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUI.SubHeadingFont); + TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(permissionLabel, consoleCommandLabel); var permissionContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedPlayerFrame.RectTransform), isHorizontal: true) @@ -2320,7 +2320,7 @@ namespace Barotrauma RelativeSpacing = 0.05f }; - new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerLeft.RectTransform), TextManager.Get("all", fallBackTag: "clientpermission.all")) + new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerLeft.RectTransform), TextManager.Get("all", "clientpermission.all")) { Enabled = !myClient, OnSelected = (tickbox) => @@ -2351,7 +2351,7 @@ namespace Barotrauma if (permission == ClientPermissions.None || permission == ClientPermissions.All) continue; var permissionTick = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), permissionsBox.Content.RectTransform), - TextManager.Get("ClientPermission." + permission), font: GUI.SmallFont) + TextManager.Get("ClientPermission." + permission), font: GUIStyle.SmallFont) { UserData = permission, Selected = selectedClient.HasPermission(permission), @@ -2387,7 +2387,7 @@ namespace Barotrauma RelativeSpacing = 0.05f }; - new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerRight.RectTransform), TextManager.Get("all", fallBackTag: "clientpermission.all")) + new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerRight.RectTransform), TextManager.Get("all", "clientpermission.all")) { Enabled = !myClient, OnSelected = (tickbox) => @@ -2415,7 +2415,7 @@ namespace Barotrauma foreach (DebugConsole.Command command in DebugConsole.Commands) { var commandTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), commandList.Content.RectTransform), - command.names[0], font: GUI.SmallFont) + command.names[0], font: GUIStyle.SmallFont) { Selected = selectedClient.PermittedConsoleCommands.Contains(command), Enabled = !myClient, @@ -2521,7 +2521,7 @@ namespace Barotrauma viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; viewSteamProfileButton.OnClicked = (bt, userdata) => { - Steamworks.SteamFriends.OpenWebOverlay("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); + SteamManager.OverlayCustomURL("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); return true; }; } @@ -2605,26 +2605,22 @@ namespace Barotrauma if (GameMain.Client == null) { return; } - string currMicStyle = micIcon.Style.Element.Name.LocalName; + Identifier currMicStyle = micIcon.Style.Element.NameAsIdentifier(); - string targetMicStyle = "GUIMicrophoneEnabled"; - if (GameMain.Config.CaptureDeviceNames == null) + Identifier targetMicStyle = "GUIMicrophoneEnabled".ToIdentifier(); + var voipCaptureDeviceNames = VoipCapture.CaptureDeviceNames; + if (voipCaptureDeviceNames.Count == 0) { - GameMain.Config.CaptureDeviceNames = OpenAL.Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + targetMicStyle = "GUIMicrophoneUnavailable".ToIdentifier(); + } + else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) + { + targetMicStyle = "GUIMicrophoneDisabled".ToIdentifier(); } - if (GameMain.Config.CaptureDeviceNames.Count == 0) + if (targetMicStyle != currMicStyle) { - targetMicStyle = "GUIMicrophoneUnavailable"; - } - else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) - { - targetMicStyle = "GUIMicrophoneDisabled"; - } - - if (!targetMicStyle.Equals(currMicStyle, StringComparison.OrdinalIgnoreCase)) - { - GUI.Style.Apply(micIcon, targetMicStyle); + GUIStyle.Apply(micIcon, targetMicStyle); } foreach (GUIComponent child in PlayerList.Content.Children) @@ -2672,12 +2668,11 @@ namespace Barotrauma JobSelectionFrame.Visible = false; } - if (GUI.MouseOn?.UserData is Pair jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") + if (GUI.MouseOn?.UserData is JobVariant jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") { - var prevVisibleVariant = jobVariantTooltip?.UserData as Pair; - if (jobVariantTooltip == null || prevVisibleVariant.First != jobPrefab.First || prevVisibleVariant.Second != jobPrefab.Second) + if (!(jobVariantTooltip?.UserData is JobVariant prevVisibleVariant) || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) { - CreateJobVariantTooltip(jobPrefab.First, jobPrefab.Second, GUI.MouseOn.Parent); + CreateJobVariantTooltip(jobPrefab.Prefab, jobPrefab.Variant, GUI.MouseOn.Parent); } } if (jobVariantTooltip != null) @@ -2730,9 +2725,9 @@ namespace Barotrauma publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); } - private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, Pair jobPrefab, int itemsPerRow) + private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) { - var itemIdentifiers = jobPrefab.First.PreviewItems[jobPrefab.Second] + var itemIdentifiers = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant] .Where(it => it.ShowPreview) .Select(it => it.ItemIdentifier) .Distinct(); @@ -2753,8 +2748,8 @@ namespace Barotrauma } int i = 0; Rectangle tooltipRect = Rectangle.Empty; - string tooltip = null; - foreach (var itemIdentifier in itemIdentifiers) + LocalizedString tooltip = null; + foreach (Identifier itemIdentifier in itemIdentifiers) { if (!(MapEntityPrefab.Find(null, identifier: itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)) { continue; } @@ -2769,15 +2764,15 @@ namespace Barotrauma scale: slotSize.X / (float)Inventory.SlotSpriteSmall.SourceRect.Width, color: slotRect.Contains(PlayerInput.MousePosition) ? Color.White : Color.White * 0.6f); - Sprite icon = itemPrefab.InventoryIcon ?? itemPrefab.sprite; + Sprite icon = itemPrefab.InventoryIcon ?? itemPrefab.Sprite; float iconScale = Math.Min(Math.Min(slotSize.X / icon.size.X, slotSize.Y / icon.size.Y), 2.0f) * 0.9f; icon.Draw(spriteBatch, slotPos + slotSize.ToVector2() * 0.5f, scale: iconScale); - int count = jobPrefab.First.PreviewItems[jobPrefab.Second].Count(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier); + int count = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant].Count(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier); if (count > 1) { string itemCountText = "x" + count; - GUI.Font.DrawString(spriteBatch, itemCountText, slotPos + slotSize.ToVector2() - GUI.Font.MeasureString(itemCountText) - Vector2.UnitX * 5, Color.White); + GUIStyle.Font.DrawString(spriteBatch, itemCountText, slotPos + slotSize.ToVector2() - GUIStyle.Font.MeasureString(itemCountText) - Vector2.UnitX * 5, Color.White); } if (slotRect.Contains(PlayerInput.MousePosition)) @@ -2787,7 +2782,7 @@ namespace Barotrauma } i++; } - if (!string.IsNullOrEmpty(tooltip)) + if (!tooltip.IsNullOrEmpty()) { GUIComponent.DrawToolTip(spriteBatch, tooltip, tooltipRect); } @@ -2803,11 +2798,10 @@ namespace Barotrauma } GUITextBlock msg = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), chatBox.Content.RectTransform), - text: ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender, + text: RichString.Rich(ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender), textColor: message.Color, color: ((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, - wrap: true, font: GUI.SmallFont, - parseRichText: true) + wrap: true, font: GUIStyle.SmallFont) { UserData = message, CanBeFocused = false @@ -2876,14 +2870,25 @@ namespace Barotrauma }, OnSliderReleased = SaveHead }; - return false; } private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); private bool StoreHead(bool save) { - GameMain.Config.PlayerCharacterCustomization = GameMain.Client.CharacterInfo.Head; + var info = GameMain.Client.CharacterInfo; + + var characterConfig = MultiplayerPreferences.Instance; + + characterConfig.TagSet.Clear(); characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet); + characterConfig.HairIndex = info.Head.HairIndex; + characterConfig.BeardIndex = info.Head.BeardIndex; + characterConfig.MoustacheIndex = info.Head.MoustacheIndex; + characterConfig.FaceAttachmentIndex = info.Head.FaceAttachmentIndex; + characterConfig.HairColor = info.Head.HairColor; + characterConfig.FacialHairColor = info.Head.FacialHairColor; + characterConfig.SkinColor = info.Head.SkinColor; + if (save) { if (GameMain.GameSession?.IsRunning ?? false) @@ -2891,14 +2896,14 @@ namespace Barotrauma TabMenu.PendingChanges = true; CreateChangesPendingText(); } - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); } return true; } private bool SwitchJob(GUIButton _, object obj) { - if (JobList == null) { return false; } + if (JobList == null || GameMain.Client == null) { return false; } int childIndex = JobList.SelectedIndex; var child = JobList.SelectedComponent; @@ -2906,11 +2911,11 @@ namespace Barotrauma bool moveToNext = obj != null; - var jobPrefab = (obj as Pair)?.First; + var jobPrefab = (obj as JobVariant)?.Prefab; var prevObj = child.UserData; - var existingChild = JobList.Content.FindChild(d => (d.UserData is Pair prefab) && (prefab.First == jobPrefab)); + var existingChild = JobList.Content.FindChild(d => (d.UserData is JobVariant prefab) && (prefab.Prefab == jobPrefab)); if (existingChild != null && obj != null) { existingChild.UserData = prevObj; @@ -2981,13 +2986,13 @@ namespace Barotrauma GUIButton jobButton = null; var availableJobs = JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => !(c.UserData is Pair prefab) || prefab.First != jobPrefab) - ).Select(j => new Pair(j, 0)); + jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => !(c.UserData is JobVariant prefab) || prefab.Prefab != jobPrefab) + ).Select(j => new JobVariant(j, 0)); availableJobs = availableJobs.Concat( JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is Pair prefab) && prefab.First == jobPrefab) - ).Select(j => JobList.Content.FindChild(c => (c.UserData is Pair prefab) && prefab.First == j).UserData as Pair)); + jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab) + ).Select(j => (JobVariant)JobList.Content.FindChild(c => (c.UserData is JobVariant prefab) && prefab.Prefab == j).UserData)); availableJobs = availableJobs.ToList(); @@ -3012,11 +3017,11 @@ namespace Barotrauma }; itemsInRow++; - var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.First, selectedByPlayer: false); + var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.Prefab, selectedByPlayer: false); if (images != null && images.Length > 1) { - jobPrefab.Second = Math.Min(jobPrefab.Second, images.Length); - int currVisible = jobPrefab.Second; + jobPrefab.Variant = Math.Min(jobPrefab.Variant, images.Length); + int currVisible = jobPrefab.Variant; GUIButton currSelected = null; for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { @@ -3029,7 +3034,7 @@ namespace Barotrauma variantButton.OnClicked = (btn, obj) => { if (currSelected != null) { currSelected.Selected = false; } - int k = ((Pair)obj).Second; + int k = ((JobVariant)obj).Variant; btn.Parent.UserData = obj; for (int j = 0; j < images.Length; j++) { @@ -3062,14 +3067,14 @@ namespace Barotrauma private GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer) { GUIFrame innerFrame = null; - List outfitPreviews = jobPrefab.GetJobOutfitSprites(Gender.Male, useInventoryIcon: true, out var maxDimensions); + List outfitPreviews = jobPrefab.GetJobOutfitSprites(CharacterPrefab.HumanPrefab.CharacterInfoPrefab, useInventoryIcon: true, out var maxDimensions); innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.85f, parent.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - GUIImage[][] retVal = new GUIImage[0][]; + GUIImage[][] retVal = Array.Empty(); if (outfitPreviews != null && outfitPreviews.Any()) { retVal = new GUIImage[outfitPreviews.Count][]; @@ -3212,7 +3217,7 @@ namespace Barotrauma { CampaignSetupFrame.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), - TextManager.Get("campaignstarting"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + TextManager.Get("campaignstarting"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); } } } @@ -3284,9 +3289,9 @@ namespace Barotrauma private bool ViewJobInfo(GUIButton button, object obj) { - if (!(button.UserData is Pair jobPrefab)) { return false; } + if (!(button.UserData is JobVariant jobPrefab)) { return false; } - JobInfoFrame = jobPrefab.First.CreateInfoFrame(out GUIComponent buttonContainer); + JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(out GUIComponent buttonContainer); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { @@ -3315,7 +3320,7 @@ namespace Barotrauma /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } jobPreferenceSprites.Clear();*/ - List> jobNamePreferences = new List>(); + List jobPreferences = new List(); bool disableNext = false; for (int i = 0; i < listBox.Content.CountChildren; i++) @@ -3325,15 +3330,15 @@ namespace Barotrauma slot.ClearChildren(); slot.CanBeFocused = !disableNext; - if (slot.UserData is Pair jobPrefab) + if (slot.UserData is JobVariant jobPrefab) { - var images = AddJobSpritesToGUIComponent(slot, jobPrefab.First, selectedByPlayer: true); + var images = AddJobSpritesToGUIComponent(slot, jobPrefab.Prefab, selectedByPlayer: true); for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { foreach (GUIImage image in images[variantIndex]) { //jobPreferenceSprites.Add(image.Sprite); - int selectedVariantIndex = Math.Min(jobPrefab.Second, images.Length); + int selectedVariantIndex = Math.Min(jobPrefab.Variant, images.Length); image.Visible = images.Length == 1 || selectedVariantIndex == variantIndex; } if (images.Length > 1) @@ -3372,14 +3377,14 @@ namespace Barotrauma } }; - jobNamePreferences.Add(new Pair(jobPrefab.First.Identifier, jobPrefab.Second)); + jobPreferences.Add(new MultiplayerPreferences.JobPreference(jobPrefab.Prefab.Identifier, jobPrefab.Variant)); } else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.6f), slot.RectTransform), (i + 1).ToString(), textColor: Color.White * (disableNext ? 0.15f : 0.5f), textAlignment: Alignment.Center, - font: GUI.LargeFont) + font: GUIStyle.LargeFont) { CanBeFocused = false }; @@ -3387,7 +3392,7 @@ namespace Barotrauma if (!disableNext) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), slot.RectTransform, Anchor.BottomCenter), TextManager.Get("clicktoselectjob"), - font: GUI.SmallFont, + font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center) { @@ -3400,7 +3405,7 @@ namespace Barotrauma } GameMain.Client.ForceNameAndJobUpdate(); - if (!GameMain.Config.AreJobPreferencesEqual(jobNamePreferences)) + if (!MultiplayerPreferences.Instance.AreJobPreferencesEqual(jobPreferences)) { if (GameMain.GameSession?.IsRunning ?? false) { @@ -3408,12 +3413,13 @@ namespace Barotrauma CreateChangesPendingText(); } - GameMain.Config.JobPreferences = jobNamePreferences; - GameMain.Config.SaveNewPlayerConfig(); + MultiplayerPreferences.Instance.JobPreferences.Clear(); + MultiplayerPreferences.Instance.JobPreferences.AddRange(jobPreferences); + GameSettings.SaveCurrentConfig(); } } - private GUIButton CreateJobVariantButton(Pair jobPrefab, int variantIndex, int variantCount, GUIComponent slot) + private GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot) { float relativeSize = 0.15f; @@ -3421,8 +3427,8 @@ namespace Barotrauma { RelativeOffset = new Vector2(relativeSize * 1.3f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) }, (variantIndex + 1).ToString(), style: "JobVariantButton") { - Selected = jobPrefab.Second == variantIndex, - UserData = new Pair(jobPrefab.First, variantIndex), + Selected = jobPrefab.Variant == variantIndex, + UserData = new JobVariant(jobPrefab.Prefab, variantIndex), }; return btn; @@ -3440,6 +3446,7 @@ namespace Barotrauma public static bool operator ==(FailedSubInfo a, FailedSubInfo b) => StringsEqual(a.Name, b.Name) && StringsEqual(a.Hash, b.Hash); + public static bool operator !=(FailedSubInfo a, FailedSubInfo b) => !(a == b); } @@ -3462,7 +3469,7 @@ namespace Barotrauma } SubmarineInfo sub = subList.Content.Children - .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.Hash == md5Hash)? + .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.StringRepresentation == md5Hash)? .UserData as SubmarineInfo; //matching sub found and already selected, all good @@ -3473,7 +3480,7 @@ namespace Barotrauma CreateSubPreview(sub); } - if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) + if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.StringRepresentation == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) { return true; } @@ -3507,7 +3514,7 @@ namespace Barotrauma FailedSelectedShuttle = null; //hashes match, all good - if (sub.MD5Hash?.Hash == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) + if (sub.MD5Hash?.StringRepresentation == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) { return true; } @@ -3525,21 +3532,23 @@ namespace Barotrauma FailedSelectedShuttle = new FailedSubInfo(subName, md5Hash); } - string errorMsg = ""; + LocalizedString errorMsg = ""; if (sub == null || !SubmarineInfo.SavedSubmarines.Contains(sub)) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", subName) + " "; } - else if (sub.MD5Hash?.Hash == null) + else if (sub.MD5Hash?.StringRepresentation == null) { errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", subName) + " "; GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); - if (textBlock != null) { textBlock.TextColor = GUI.Style.Red; } + if (textBlock != null) { textBlock.TextColor = GUIStyle.Red; } } else { - errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]" , "[myhash]", "[serverhash]" }, - new string[3] { sub.Name, sub.MD5Hash.ShortHash, Md5Hash.GetShortHash(md5Hash) }) + " "; + errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", + ("[subname]", sub.Name), + ("[myhash]", sub.MD5Hash.ShortRepresentation), + ("[serverhash]", Md5Hash.GetShortHash(md5Hash))) + " "; } //already showing a message about the same sub @@ -3553,7 +3562,7 @@ namespace Barotrauma errorMsg += TextManager.Get("DownloadSubQuestion"); var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "request" + subName }; @@ -3590,7 +3599,7 @@ namespace Barotrauma return false; } - SubmarineInfo purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name && s.MD5Hash?.Hash == serverSubmarine.MD5Hash?.Hash); + SubmarineInfo purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name && s.MD5Hash?.StringRepresentation == serverSubmarine.MD5Hash?.StringRepresentation); if (purchasableSub != null) { return true; @@ -3598,21 +3607,23 @@ namespace Barotrauma purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name); - string errorMsg = ""; + LocalizedString errorMsg = ""; if (purchasableSub == null) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", serverSubmarine.Name) + " "; } - else if (purchasableSub.MD5Hash?.Hash == null) + else if (purchasableSub.MD5Hash?.StringRepresentation == null) { errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", serverSubmarine.Name) + " "; /*GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); - if (textBlock != null) { textBlock.TextColor = GUI.Style.Red; }*/ + if (textBlock != null) { textBlock.TextColor = GUIStyle.Red; }*/ } else { - errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]", "[myhash]", "[serverhash]" }, - new string[3] { purchasableSub.Name, purchasableSub.MD5Hash.ShortHash, Md5Hash.GetShortHash(serverSubmarine.MD5Hash.Hash) }) + " "; + errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", + ("[subname]", purchasableSub.Name), + ("[myhash]", purchasableSub.MD5Hash.ShortRepresentation), + ("[serverhash]", Md5Hash.GetShortHash(serverSubmarine.MD5Hash.StringRepresentation))) + " "; } errorMsg += TextManager.Get("DownloadSubQuestion"); @@ -3624,11 +3635,11 @@ namespace Barotrauma } var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "request" + serverSubmarine.Name }; - requestFileBox.Buttons[0].UserData = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.Hash); + requestFileBox.Buttons[0].UserData = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.StringRepresentation); requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => { @@ -3675,7 +3686,7 @@ namespace Barotrauma private void CreateSubmarineVisibilityMenu() { var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "", - buttons: Array.Empty(), + buttons: Array.Empty(), relativeSize: new Vector2(0.75f, 0.75f)); messageBox.Content.ChildAnchor = Anchor.TopCenter; var columns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), messageBox.Content.RectTransform), isHorizontal: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 728752cca..418ee66ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using Barotrauma.Particles; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using System.Text; using Barotrauma.Extensions; @@ -110,7 +111,7 @@ namespace Barotrauma }; var emitterListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), paddedRightPanel.RectTransform)); - new SerializableEntityEditor(emitterListBox.Content.RectTransform, emitterProperties, false, true, elementHeight: 20, titleFont: GUI.SubHeadingFont); + new SerializableEntityEditor(emitterListBox.Content.RectTransform, emitterProperties, false, true, elementHeight: 20, titleFont: GUIStyle.SubHeadingFont); var listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), paddedRightPanel.RectTransform)); @@ -120,8 +121,8 @@ namespace Barotrauma UserData = "filterarea" }; - filterLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font) { IgnoreLayoutGroups = true }; - filterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font); + filterLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font) { IgnoreLayoutGroups = true }; + filterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font); filterBox.OnTextChanged += (textBox, text) => { FilterEmitters(text); return true; }; new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), filterArea.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton") { @@ -136,7 +137,7 @@ namespace Barotrauma emitterPrefab = new ParticleEmitterPrefab(selectedPrefab, emitterProperties); emitter = new ParticleEmitter(emitterPrefab); listBox.ClearChildren(); - new SerializableEntityEditor(listBox.Content.RectTransform, selectedPrefab, false, true, elementHeight: 20, titleFont: GUI.SubHeadingFont); + new SerializableEntityEditor(listBox.Content.RectTransform, selectedPrefab, false, true, elementHeight: 20, titleFont: GUIStyle.SubHeadingFont); //listBox.Content.RectTransform.NonScaledSize = particlePrefabEditor.RectTransform.NonScaledSize; //listBox.UpdateScrollBarSize(); return true; @@ -167,7 +168,7 @@ namespace Barotrauma foreach (ParticlePrefab particlePrefab in particlePrefabs) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), prefabList.Content.RectTransform) { MinSize = new Point(0, 20) }, - particlePrefab.DisplayName) + particlePrefab.Name) { Padding = Vector4.Zero, UserData = particlePrefab @@ -196,7 +197,7 @@ namespace Barotrauma private void SerializeAll() { Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -218,7 +219,7 @@ namespace Barotrauma NewLineOnAttributes = true }; - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -265,7 +266,7 @@ namespace Barotrauma }; XElement originalElement = null; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -273,7 +274,7 @@ namespace Barotrauma var prefabList = GameMain.ParticleManager.GetPrefabList(); foreach (ParticlePrefab otherPrefab in prefabList) { - foreach (XElement subElement in doc.Root.Elements()) + foreach (var subElement in doc.Root.Elements()) { if (!subElement.Name.ToString().Equals(prefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(prefab, subElement, true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs index 5aeea7263..3c63217cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs @@ -9,7 +9,7 @@ namespace Barotrauma { private Sprite backgroundSprite; private RoundSummary roundSummary; - private string loadText; + private LocalizedString loadText; private RectTransform prevGuiElementParent; @@ -47,9 +47,9 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); - string loadingText = loadText + new string('.', (int)Timing.TotalTime % 3 + 1); - Vector2 textSize = GUI.LargeFont.MeasureString(loadText); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.95f) - textSize / 2, loadingText, Color.White, font: GUI.LargeFont); + LocalizedString loadingText = loadText + new string('.', (int)Timing.TotalTime % 3 + 1); + Vector2 textSize = GUIStyle.LargeFont.MeasureString(loadText); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.95f) - textSize / 2, loadingText, Color.White, font: GUIStyle.LargeFont); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 3093092c7..356b8878c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace Barotrauma { - partial class Screen + abstract partial class Screen { private GUIFrame frame; public GUIFrame Frame @@ -70,6 +70,7 @@ namespace Barotrauma public virtual void Release() { + if (frame is null) { return; } frame.RectTransform.Parent = null; frame = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 461a8ba29..5ec47b586 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -44,34 +44,6 @@ namespace Barotrauma private GUIButton friendsDropdownButton; private GUIListBox friendsDropdown; - //Workshop downloads - public struct PendingWorkshopDownload - { - public readonly string ExpectedHash; - public readonly ulong Id; - public readonly Steamworks.Ugc.Item? Item; - - public PendingWorkshopDownload(string expectedHash, Steamworks.Ugc.Item item) - { - ExpectedHash = expectedHash; - Item = item; - Id = item.Id; - } - - public PendingWorkshopDownload(string expectedHash, ulong id) - { - ExpectedHash = expectedHash; - Item = null; - Id = id; - } - } - - private GUIFrame workshopDownloadsFrame = null; - private Steamworks.Ugc.Item? currentlyDownloadingWorkshopItem = null; - private Dictionary pendingWorkshopDownloads = null; - private string autoConnectName; - private string autoConnectEndpoint; - private enum TernaryOption { Any, @@ -84,7 +56,7 @@ namespace Barotrauma public UInt64 SteamID; public string Name; public Sprite Sprite; - public string StatusText; + public LocalizedString StatusText; public bool PlayingThisGame; public bool PlayingAnotherGame; public string ConnectName; @@ -95,7 +67,7 @@ namespace Barotrauma { get { - return PlayingThisGame && !string.IsNullOrWhiteSpace(StatusText) && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); + return PlayingThisGame && !StatusText.IsNullOrWhiteSpace() && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); } } } @@ -182,10 +154,10 @@ namespace Barotrauma private GUITickBox filterFull; private GUITickBox filterEmpty; private GUITickBox filterWhitelisted; - private Dictionary ternaryFilters; - private Dictionary filterTickBoxes; - private Dictionary playStyleTickBoxes; - private Dictionary gameModeTickBoxes; + private Dictionary ternaryFilters; + private Dictionary filterTickBoxes; + private Dictionary playStyleTickBoxes; + private Dictionary gameModeTickBoxes; private GUITickBox filterOffensive; //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. @@ -212,7 +184,7 @@ namespace Barotrauma CreateUI(); } - private void AddTernaryFilter(RectTransform parent, float elementHeight, string tag, Action valueSetter) + private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) { var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) { @@ -239,7 +211,7 @@ namespace Barotrauma { UserData = TextManager.Get("servertag." + tag + ".label") }; - GUI.Style.Apply(filterLabel, "GUITextBlock", null); + GUIStyle.Apply(filterLabel, "GUITextBlock", null); var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); @@ -249,6 +221,7 @@ namespace Barotrauma dropDown.OnSelected = (_, data) => { valueSetter((TernaryOption)data); FilterServers(); + StoreServerFilters(); return true; }; @@ -271,10 +244,10 @@ namespace Barotrauma var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUI.LargeFont) + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) { Padding = Vector4.Zero, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, AutoScaleHorizontal = true }; @@ -282,10 +255,10 @@ namespace Barotrauma var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont); ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") { - Text = GameMain.Config.PlayerName, + Text = MultiplayerPreferences.Instance.PlayerName, MaxTextLength = Client.MaxNameLength, OverflowClip = true }; @@ -296,7 +269,7 @@ namespace Barotrauma } ClientNameBox.OnTextChanged += (textbox, text) => { - GameMain.Config.PlayerName = text; + MultiplayerPreferences.Instance.PlayerName = text; return true; }; @@ -366,7 +339,7 @@ namespace Barotrauma }; float elementHeight = 0.05f; - var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUI.SubHeadingFont) + var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, AutoScaleHorizontal = true, @@ -405,115 +378,79 @@ namespace Barotrauma }; filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); - ternaryFilters = new Dictionary(); - filterTickBoxes = new Dictionary(); + ternaryFilters = new Dictionary(); + filterTickBoxes = new Dictionary(); - filterSameVersion = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterSameVersion")) + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) { - UserData = TextManager.Get("FilterSameVersion"), - Selected = true, - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterSameVersion", filterSameVersion); + text ??= TextManager.Get(key); + var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + { + UserData = text, + Selected = defaultState, + ToolTip = addTooltip ? text : null, + OnSelected = (tickBox) => + { + FilterServers(); + StoreServerFilters(); + return true; + } + }; + filterTickBoxes.Add(key, tickBox); + return tickBox; + } - filterPassword = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterPassword")) - { - UserData = TextManager.Get("FilterPassword"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterPassword", filterPassword); - - filterIncompatible = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterIncompatibleServers")) - { - UserData = TextManager.Get("FilterIncompatibleServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterIncompatibleServers", filterIncompatible); - - filterFull = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterFullServers")) - { - UserData = TextManager.Get("FilterFullServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterFullServers", filterFull); - - filterEmpty = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterEmptyServers")) - { - UserData = TextManager.Get("FilterEmptyServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterEmptyServers", filterEmpty); - - filterWhitelisted = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterWhitelistedServers")) - { - UserData = TextManager.Get("FilterWhitelistedServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterWhitelistedServers", filterWhitelisted); - - filterOffensive = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterOffensiveServers")) - { - UserData = TextManager.Get("FilterOffensiveServers"), - ToolTip = TextManager.Get("FilterOffensiveServersToolTip"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterOffensiveServers", filterOffensive); + filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true); + filterPassword = addTickBox("FilterPassword".ToIdentifier()); + filterIncompatible = addTickBox("FilterIncompatibleServers".ToIdentifier()); + filterFull = addTickBox("FilterFullServers".ToIdentifier()); + filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); + filterWhitelisted = addTickBox("FilterWhitelistedServers".ToIdentifier()); + filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); // Filter Tags - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma", (value) => { filterKarmaValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors", (value) => { filterTraitorValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire", (value) => { filterFriendlyFireValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip", (value) => { filterVoipValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded", (value) => { filterModdedValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; }); // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - playStyleTickBoxes = new Dictionary(); + playStyleTickBoxes = new Dictionary(); foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag." + playStyle)) - { - ToolTip = TextManager.Get("servertag." + playStyle), - Selected = true, - OnSelected = (tickBox) => { FilterServers(); return true; }, - UserData = playStyle - }; - playStyleTickBoxes.Add("servertag." + playStyle, selectionTick); - filterTickBoxes.Add("servertag." + playStyle, selectionTick); + var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true); + selectionTick.UserData = playStyle; + playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick); } // Game mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUI.SubHeadingFont) { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - gameModeTickBoxes = new Dictionary(); + gameModeTickBoxes = new Dictionary(); foreach (GameModePreset mode in GameModePreset.List) { - if (mode.IsSinglePlayer) continue; + if (mode.IsSinglePlayer) { continue; } - var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), mode.Name) - { - ToolTip = mode.Name, - Selected = true, - OnSelected = (tickBox) => { FilterServers(); return true; }, - UserData = mode.Identifier - }; + var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true); + selectionTick.UserData = mode.Identifier; gameModeTickBoxes.Add(mode.Identifier, selectionTick); - filterTickBoxes.Add(mode.Identifier, selectionTick); } filters.Content.RectTransform.SizeChanged += () => { filters.Content.RectTransform.RecalculateChildren(true, true); - filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData as string); + filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString()); gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); GUITextBlock.AutoScaleAndNormalize( @@ -543,7 +480,7 @@ namespace Barotrauma text: TextManager.Get(columnLabel[i]), textAlignment: Alignment.Center, style: "GUIButtonSmall") { ToolTip = TextManager.Get(columnLabel[i]), - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = columnLabel[i], OnClicked = SortList }; @@ -734,7 +671,7 @@ namespace Barotrauma XDocument playStylesDoc = XMLExtensions.TryLoadXml("Content/UI/Server/PlayStyles.xml"); - XElement rootElement = playStylesDoc.Root; + var rootElement = playStylesDoc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); foreach (var element in rootElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) @@ -839,7 +776,7 @@ namespace Barotrauma info.LobbyID = SteamManager.CurrentLobbyID; info.IP = ip; info.Port = port; - info.GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? ""; + info.GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty; info.GameStarted = Screen.Selected != GameMain.NetLobbyScreen; info.GameVersion = GameMain.Version.ToString(); info.MaxPlayers = serverSettings.MaxPlayers; @@ -1018,21 +955,21 @@ namespace Barotrauma { base.Select(); - ContentPackagesByWorkshopId = ContentPackage.AllPackages + ContentPackagesByWorkshopId = ContentPackageManager.AllPackages .Select(p => new KeyValuePair(p.SteamWorkshopId, p)) .Where(p => p.Key != 0) .GroupBy(x => x.Key).Select(g => g.First()) .ToImmutableDictionary(); - ContentPackagesByHash = ContentPackage.AllPackages - .Select(p => new KeyValuePair(p.MD5hash.Hash, p)) + ContentPackagesByHash = ContentPackageManager.AllPackages + .Select(p => new KeyValuePair(p.Hash.StringRepresentation, p)) .GroupBy(x => x.Key).Select(g => g.First()) .ToImmutableDictionary(); SelectedTab = ServerListTab.All; - LoadServerFilters(GameMain.Config.ServerFilterElement); - if (GameSettings.ShowOffensiveServerPrompt) + GameMain.ServerListScreen.LoadServerFilters(); + if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt) { - var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("filteroffensiveserversprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("filteroffensiveserversprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => { filterOffensive.Selected = true; @@ -1040,7 +977,10 @@ namespace Barotrauma return true; }; filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; - GameSettings.ShowOffensiveServerPrompt = false; + + var config = GameSettings.CurrentConfig; + config.ShowOffensiveServerPrompt = false; + GameSettings.SetCurrentConfig(config); } Steamworks.SteamMatchmaking.ResetActions(); @@ -1060,10 +1000,7 @@ namespace Barotrauma ContentPackagesByHash = ImmutableDictionary.Empty; base.Deselect(); - GameMain.Config.SaveNewPlayerConfig(); - - pendingWorkshopDownloads?.Clear(); - workshopDownloadsFrame = null; + GameSettings.SaveCurrentConfig(); } public override void Update(double deltaTime) @@ -1083,62 +1020,6 @@ namespace Barotrauma friendsDropdown.Visible = false; } } - - if (currentlyDownloadingWorkshopItem == null) - { - if (pendingWorkshopDownloads?.Any() ?? false) - { - Steamworks.Ugc.Item? item = pendingWorkshopDownloads.Values.FirstOrDefault(it => it.Item != null).Item; - if (item != null) - { - ulong itemId = item.Value.Id; - currentlyDownloadingWorkshopItem = item; - SteamManager.ForceRedownload(item.Value.Id, () => - { - if (!(item?.IsSubscribed ?? false)) - { - TaskPool.Add("SubscribeToServerMod", item?.Subscribe(), (t) => { }); - } - PendingWorkshopDownload clearedDownload = pendingWorkshopDownloads[itemId]; - pendingWorkshopDownloads.Remove(itemId); - currentlyDownloadingWorkshopItem = null; - - void onInstall(ContentPackage resultingPackage) - { - if (!resultingPackage.MD5hash.Hash.Equals(clearedDownload.ExpectedHash)) - { - workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Red); - CancelWorkshopDownloads(); - GameMain.Client?.Disconnect(); - GameMain.Client = null; - new GUIMessageBox( - TextManager.Get("ConnectionLost"), - TextManager.GetWithVariable("DisconnectMessage.MismatchedWorkshopMod", "[incompatiblecontentpackage]", $"\"{resultingPackage.Name}\" (hash {resultingPackage.MD5hash.ShortHash})")); - } - } - - if (SteamManager.CheckWorkshopItemInstalled(item)) - { - SteamManager.UninstallWorkshopItem(item, false, out _); - } - if (SteamManager.InstallWorkshopItem(item, out string errorMsg, enableContentPackage: false, suppressInstallNotif: true, onInstall: onInstall)) - { - workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Green); - } - else - { - workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Red); - DebugConsole.ThrowError(errorMsg); - } - }); - } - } - else if (!string.IsNullOrEmpty(autoConnectEndpoint)) - { - JoinServer(autoConnectEndpoint, autoConnectName); - autoConnectEndpoint = null; - } - } } private void FilterServers() @@ -1207,8 +1088,8 @@ namespace Barotrauma foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { - var gameMode = (string)tickBox.UserData; - if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) + var gameMode = (Identifier)tickBox.UserData; + if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode == gameMode) { child.Visible = false; break; @@ -1253,7 +1134,7 @@ namespace Barotrauma private void ShowDirectJoinPrompt() { var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", - new string[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, + new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); msgBox.Content.ChildAnchor = Anchor.TopCenter; @@ -1597,7 +1478,7 @@ namespace Barotrauma { friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") { - Font = GUI.GlobalFont, + Font = GUIStyle.GlobalFont, OnClicked = (button, udt) => { friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); @@ -1680,10 +1561,10 @@ namespace Barotrauma var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; - if (friend.PlayingThisGame) { textBlock.TextColor = GUI.Style.Green; } - if (friend.PlayingAnotherGame) { textBlock.TextColor = GUI.Style.Blue; } + if (friend.PlayingThisGame) { textBlock.TextColor = GUIStyle.Green; } + if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } if (friend.InServer) { @@ -1744,7 +1625,7 @@ namespace Barotrauma } recentServers.Concat(favoriteServers).ForEach(si => si.OwnerVerified = false); - if (GameMain.Config.UseSteamMatchmaking) + if (GameSettings.CurrentConfig.UseSteamMatchmaking) { serverList.ClearChildren(); if (!SteamManager.GetServers(AddToServerList, ServerQueryFinished)) @@ -1768,11 +1649,6 @@ namespace Barotrauma scanServersButton.Enabled = true; } } - else - { - CoroutineManager.StartCoroutine(SendMasterServerRequest()); - waitingForRefresh = false; - } refreshDisableTimer = DateTime.Now + AllowedRefreshInterval; @@ -1998,14 +1874,14 @@ namespace Barotrauma if (serverInfo.LobbyID == 0 && (string.IsNullOrWhiteSpace(serverInfo.IP) || string.IsNullOrWhiteSpace(serverInfo.Port))) { - string toolTip = TextManager.Get("ServerOffline"); + LocalizedString toolTip = TextManager.Get("ServerOffline"); serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f; } - else if (GameMain.Config.UseSteamMatchmaking && serverInfo.RespondedToSteamQuery.HasValue && serverInfo.RespondedToSteamQuery.Value == false) + else if (GameSettings.CurrentConfig.UseSteamMatchmaking && serverInfo.RespondedToSteamQuery.HasValue && serverInfo.RespondedToSteamQuery.Value == false) { - string toolTip = TextManager.Get("ServerListNoSteamQueryResponse"); + LocalizedString toolTip = TextManager.Get("ServerListNoSteamQueryResponse"); compatibleBox.Selected = false; serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.8f; @@ -2014,7 +1890,7 @@ namespace Barotrauma else if (string.IsNullOrEmpty(serverInfo.GameVersion) || !serverInfo.ContentPackageHashes.Any()) { compatibleBox.Selected = false; - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUI.Style.Orange * 0.85f, textAlignment: Alignment.Center) + new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center) { ToolTip = TextManager.Get(string.IsNullOrEmpty(serverInfo.GameVersion) ? "ServerListUnknownVersion" : @@ -2023,27 +1899,30 @@ namespace Barotrauma } else if (!compatibleBox.Selected) { - string toolTip = ""; + LocalizedString toolTip = ""; if (serverInfo.GameVersion != GameMain.Version.ToString()) + { toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion); + } for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { bool listAsIncompatible = false; if (serverInfo.ContentPackageWorkshopIds[i] == 0) { - listAsIncompatible = !GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i]); + listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i]); } else { - listAsIncompatible = GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash != serverInfo.ContentPackageHashes[i] && - cp.SteamWorkshopId == serverInfo.ContentPackageWorkshopIds[i]); + listAsIncompatible = ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation != serverInfo.ContentPackageHashes[i] && + contentPackage.SteamWorkshopId == serverInfo.ContentPackageWorkshopIds[i]); } if (listAsIncompatible) { if (toolTip != "") toolTip += "\n"; - toolTip += TextManager.GetWithVariables("ServerListIncompatibleContentPackage", new string[2] { "[contentpackage]", "[hash]" }, - new string[2] { serverInfo.ContentPackageNames[i], Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i]) }); + toolTip += TextManager.GetWithVariables("ServerListIncompatibleContentPackage", + ("[contentpackage]", serverInfo.ContentPackageNames[i]), + ("[hash]", Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i]))); } } @@ -2054,12 +1933,12 @@ namespace Barotrauma } else { - string toolTip = ""; + LocalizedString toolTip = ""; for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { - if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i])) + if (!ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i])) { - if (toolTip != "") toolTip += "\n"; + if (toolTip != "") { toolTip += "\n"; } toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", serverInfo.ContentPackageNames[i]); break; } @@ -2116,163 +1995,12 @@ namespace Barotrauma waitingForRefresh = false; } - private IEnumerable SendMasterServerRequest() - { - RestClient client = null; - try - { - client = new RestClient(NetConfig.MasterServerUrl); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error while connecting to master server", e); - } - - if (client == null) yield return CoroutineStatus.Success; - - var request = new RestRequest("masterserver2.php", Method.GET); - request.AddParameter("gamename", "barotrauma"); - request.AddParameter("action", "listservers"); - - // execute the request - masterServerResponded = false; - var restRequestHandle = client.ExecuteAsync(request, response => MasterServerCallBack(response)); - - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 8); - while (!masterServerResponded) - { - if (DateTime.Now > timeOut) - { - serverList.ClearChildren(); - restRequestHandle.Abort(); - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.Get("MasterServerTimeOutError")); - yield return CoroutineStatus.Success; - } - yield return CoroutineStatus.Running; - } - - if (masterServerResponse.ErrorException != null) - { - serverList.ClearChildren(); - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.GetWithVariable("MasterServerErrorException", "[error]", masterServerResponse.ErrorException.ToString())); - } - else if (masterServerResponse.StatusCode != HttpStatusCode.OK) - { - serverList.ClearChildren(); - - switch (masterServerResponse.StatusCode) - { - case HttpStatusCode.NotFound: - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), - TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", NetConfig.MasterServerUrl)); - break; - case HttpStatusCode.ServiceUnavailable: - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), - TextManager.Get("MasterServerErrorUnavailable")); - break; - default: - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), - TextManager.GetWithVariables("MasterServerErrorDefault", new string[2] { "[statuscode]", "[statusdescription]" }, - new string[2] { masterServerResponse.StatusCode.ToString(), masterServerResponse.StatusDescription })); - break; - } - - } - else - { - UpdateServerList(masterServerResponse.Content); - } - - yield return CoroutineStatus.Success; - - } - private void MasterServerCallBack(IRestResponse response) { masterServerResponse = response; masterServerResponded = true; } - public void DownloadWorkshopItems(IEnumerable downloads, string serverName, string endPointString) - { - if (workshopDownloadsFrame != null) { return; } - int rowCount = downloads.Count() + 2; - - autoConnectName = serverName; autoConnectEndpoint = endPointString; - - workshopDownloadsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), null, Color.Black * 0.5f); - currentlyDownloadingWorkshopItem = null; - pendingWorkshopDownloads = new Dictionary(); - - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f + 0.03f * rowCount), workshopDownloadsFrame.RectTransform, Anchor.Center, Pivot.Center)); - var innerLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, (float)rowCount / (float)(rowCount + 3)), innerFrame.RectTransform, Anchor.Center, Pivot.Center)); - - foreach (PendingWorkshopDownload entry in downloads) - { - pendingWorkshopDownloads.Add(entry.Id, entry); - - var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f / rowCount), innerLayout.RectTransform), true, Anchor.CenterLeft) - { - UserData = entry.Id - }; - TaskPool.Add("RetrieveWorkshopItemData", Steamworks.SteamUGC.QueryFileAsync(entry.Id), (t) => - { - if (t.IsFaulted) - { - TaskPool.PrintTaskExceptions(t, $"Failed to retrieve Workshop item info (ID {entry.Id})"); - return; - } - t.TryGetResult(out Steamworks.Ugc.Item? item); - - if (!item.HasValue) - { - DebugConsole.ThrowError($"Failed to find a Steam Workshop item with the ID {entry.Id}."); - return; - } - - if (pendingWorkshopDownloads.ContainsKey(entry.Id)) - { - pendingWorkshopDownloads[entry.Id] = new PendingWorkshopDownload(entry.ExpectedHash, item.Value); - - new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.67f), itemLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), item.Value.Title); - - new GUIProgressBar(new RectTransform(new Vector2(0.6f, 0.67f), itemLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), 0f, Color.Lime) - { - ProgressGetter = () => - { - if (item.Value.IsInstalled) { return 1.0f; } - else if (!item.Value.IsDownloading) { return 0.0f; } - return item.Value.DownloadAmount; - } - }; - } - }); - } - - var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 2.0f / rowCount), innerLayout.RectTransform), true, Anchor.CenterLeft) - { - UserData = "buttons" - }; - - new GUIButton(new RectTransform(new Vector2(0.3f, 0.67f), buttonLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), TextManager.Get("Cancel")) - { - OnClicked = (btn, obj) => - { - CancelWorkshopDownloads(); - return true; - } - }; - } - - public void CancelWorkshopDownloads() - { - autoConnectEndpoint = null; - autoConnectName = null; - pendingWorkshopDownloads.Clear(); - currentlyDownloadingWorkshopItem = null; - workshopDownloadsFrame = null; - } - private bool JoinServer(string endpoint, string serverName) { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) @@ -2283,8 +2011,8 @@ namespace Barotrauma return false; } - GameMain.Config.PlayerName = ClientNameBox.Text; - GameMain.Config.SaveNewPlayerConfig(); + MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; + GameSettings.SaveCurrentConfig(); CoroutineManager.StartCoroutine(ConnectToServer(endpoint, serverName), "ConnectToServer"); @@ -2301,7 +2029,7 @@ namespace Barotrauma try { #endif - GameMain.Client = new GameClient(GameMain.Config.PlayerName, serverIP, serverSteamID, serverName); + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), serverIP, serverSteamID, serverName); #if !DEBUG } catch (Exception e) @@ -2345,7 +2073,7 @@ namespace Barotrauma private Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } - return ToolBox.GradientLerp(ping / 200.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } public async Task PingServerAsync(string ip, int timeOut) @@ -2425,35 +2153,35 @@ namespace Barotrauma menu.AddToGUIUpdateList(); friendPopup?.AddToGUIUpdateList(); friendsDropdown?.AddToGUIUpdateList(); - workshopDownloadsFrame?.AddToGUIUpdateList(); } - public void SaveServerFilters(XElement element) + public void StoreServerFilters() { - element.RemoveAttributes(); - foreach (KeyValuePair filterBox in filterTickBoxes) + foreach (KeyValuePair filterBox in filterTickBoxes) { - element.Add(new XAttribute(filterBox.Key, filterBox.Value.Selected.ToString())); + ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString()); } - foreach (KeyValuePair ternaryFilter in ternaryFilters) + foreach (KeyValuePair ternaryFilter in ternaryFilters) { - element.Add(new XAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString())); + ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); } } - public void LoadServerFilters(XElement element) + public void LoadServerFilters() { - if (element == null) { return; } - - foreach (KeyValuePair filterBox in filterTickBoxes) + XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); + ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); + foreach (KeyValuePair filterBox in filterTickBoxes) { - filterBox.Value.Selected = element.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); + filterBox.Value.Selected = + ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); } - foreach (KeyValuePair ternaryFilter in ternaryFilters) + foreach (KeyValuePair ternaryFilter in ternaryFilters) { - string valueStr = element.GetAttributeString(ternaryFilter.Key, ""); - TernaryOption ternaryOption = (TernaryOption)ternaryFilter.Value.SelectedData; - Enum.TryParse(valueStr, true, out ternaryOption); + TernaryOption ternaryOption = + ServerListFilters.Instance.GetAttributeEnum( + ternaryFilter.Key, + (TernaryOption)ternaryFilter.Value.SelectedData); var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 13936faa4..95f6f37c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -14,7 +14,7 @@ using Barotrauma.IO; namespace Barotrauma { - class SpriteEditorScreen : Screen + class SpriteEditorScreen : EditorScreen { private GUIListBox textureList, spriteList; @@ -30,22 +30,19 @@ namespace Barotrauma private GUITextBlock texturePathText; private GUITextBlock xmlPathText; private GUIScrollBar zoomBar; - private List selectedSprites = new List(); - private List dirtySprites = new List(); - private Texture2D selectedTexture; - private Sprite lastSelected; + private readonly List selectedSprites = new List(); + private readonly List dirtySprites = new List(); + private Sprite selectedTexture; private Rectangle textureRect; private float zoom = 1; - private float minZoom = 0.25f; - private float maxZoom; - private int spriteCount; + private const float MinZoom = 0.25f, MaxZoom = 10.0f; private GUITextBox filterSpritesBox; private GUITextBlock filterSpritesLabel; private GUITextBox filterTexturesBox; private GUITextBlock filterTexturesLabel; - private string originLabel, positionLabel, sizeLabel; + private LocalizedString originLabel, positionLabel, sizeLabel; private bool editBackgroundColor; private Color backgroundColor = new Color(0.051f, 0.149f, 0.271f, 1.0f); @@ -92,8 +89,8 @@ namespace Barotrauma RefreshLists(); textureList.Select(firstSelected.Texture, autoScroll: false); selected.ForEachMod(s => spriteList.Select(s, autoScroll: false)); - texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath); - texturePathText.TextColor = GUI.Style.Green; + texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value); + texturePathText.TextColor = GUIStyle.Green; return true; } }; @@ -107,7 +104,7 @@ namespace Barotrauma if (selectedTexture == null) { return false; } foreach (Sprite sprite in loadedSprites) { - if (sprite.Texture != selectedTexture) { continue; } + if (sprite.FullPath != selectedTexture.FullPath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } // Not all sprites have a sourcerect defined, in which case we'll want to use the current source rect instead of an empty rect. @@ -116,7 +113,7 @@ namespace Barotrauma } ResetWidgets(); xmlPathText.Text = TextManager.Get("spriteeditor.resetsuccessful"); - xmlPathText.TextColor = GUI.Style.Green; + xmlPathText.TextColor = GUIStyle.Green; return true; } }; @@ -153,7 +150,7 @@ namespace Barotrauma Step = 0.01f, OnMoved = (scrollBar, value) => { - zoom = MathHelper.Lerp(minZoom, maxZoom, value); + zoom = MathHelper.Lerp(MinZoom, MaxZoom, value); viewAreaOffset = Point.Zero; return true; } @@ -200,8 +197,8 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ; - filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ; + filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true); filterArea.RectTransform.MinSize = filterTexturesBox.RectTransform.MinSize; filterTexturesBox.OnTextChanged += (textBox, text) => { FilterTextures(text); return true; }; @@ -209,9 +206,9 @@ namespace Barotrauma { OnSelected = (listBox, userData) => { - var previousTexture = selectedTexture; - selectedTexture = userData as Texture2D; - if (previousTexture != selectedTexture) + var previousSprite = selectedTexture; + selectedTexture = userData as Sprite; + if (previousSprite != selectedTexture) { ResetZoom(); } @@ -219,12 +216,12 @@ namespace Barotrauma { var textBlock = (GUITextBlock)child; var sprite = (Sprite)textBlock.UserData; - textBlock.TextColor = new Color(textBlock.TextColor, sprite.Texture == selectedTexture ? 1.0f : 0.4f); - if (sprite.Texture == selectedTexture) { textBlock.Visible = true; } + textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexture.FilePath ? 1.0f : 0.4f); + if (sprite.FilePath == selectedTexture.FilePath) { textBlock.Visible = true; } } - if (selectedSprites.None(s => s.Texture == selectedTexture)) + if (selectedSprites.None(s => s.FilePath == selectedTexture.FilePath)) { - spriteList.Select(loadedSprites.First(s => s.Texture == selectedTexture), autoScroll: false); + spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexture.FilePath), autoScroll: false); UpdateScrollBar(spriteList); } texturePathText.TextColor = Color.LightGray; @@ -245,8 +242,8 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; - filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; + filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true); filterArea.RectTransform.MinSize = filterSpritesBox.RectTransform.MinSize; filterSpritesBox.OnTextChanged += (textBox, text) => { FilterSprites(text); return true; }; @@ -282,7 +279,7 @@ namespace Barotrauma RelativeSpacing = 0.01f }; var fields = new GUIComponent[4]; - string[] colorComponentLabels = { TextManager.Get("spriteeditor.colorcomponentr"), TextManager.Get("spriteeditor.colorcomponentg"), TextManager.Get("spriteeditor.colorcomponentb") }; + LocalizedString[] colorComponentLabels = { TextManager.Get("spriteeditor.colorcomponentr"), TextManager.Get("spriteeditor.colorcomponentg"), TextManager.Get("spriteeditor.colorcomponentb") }; for (int i = 2; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.2f, 1), inputArea.RectTransform) @@ -291,23 +288,23 @@ namespace Barotrauma MaxSize = new Point(100, 50) }, style: null, color: Color.Black * 0.6f); var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], - font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = 0; numberInput.MaxValueInt = 255; - numberInput.Font = GUI.SmallFont; + numberInput.Font = GUIStyle.SmallFont; switch (i) { case 0: - colorLabel.TextColor = GUI.Style.Red; + colorLabel.TextColor = GUIStyle.Red; numberInput.IntValue = backgroundColor.R; numberInput.OnValueChanged += (numInput) => backgroundColor.R = (byte)(numInput.IntValue); break; case 1: - colorLabel.TextColor = GUI.Style.Green; + colorLabel.TextColor = GUIStyle.Green; numberInput.IntValue = backgroundColor.G; numberInput.OnValueChanged += (numInput) => backgroundColor.G = (byte)(numInput.IntValue); break; @@ -325,7 +322,7 @@ namespace Barotrauma { loadedSprites.ForEach(s => s.Remove()); loadedSprites.Clear(); - var contentPackages = GameMain.Config.AllEnabledPackages.ToList(); + var contentPackages = ContentPackageManager.EnabledPackages.All.ToList(); #if !DEBUG var vanilla = GameMain.VanillaContent; @@ -343,16 +340,15 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(file.Path); if (doc != null) { - LoadSprites(doc.Root); + LoadSprites(doc.Root.FromPackage(file.Path.ContentPackage)); } } } } - void LoadSprites(XElement element) + void LoadSprites(ContentXElement element) { - string[] spriteElementNames = new string[] - { + string[] spriteElementNames = { "Sprite", "DeformableSprite", "BackgroundSprite", @@ -371,34 +367,33 @@ namespace Barotrauma foreach (string spriteElementName in spriteElementNames) { - element.Elements(spriteElementName).ForEach(s => CreateSprite(s)); - element.Elements(spriteElementName.ToLowerInvariant()).ForEach(s => CreateSprite(s)); + element.GetChildElements(spriteElementName).ForEach(s => CreateSprite(s)); } element.Elements().ForEach(e => LoadSprites(e)); } - void CreateSprite(XElement element) + void CreateSprite(ContentXElement element) { string spriteFolder = ""; - string textureElement = ""; + ContentPath texturePath = null; if (element.Attribute("texture") != null) { - textureElement = element.GetAttributeString("texture", ""); + texturePath = element.GetAttributeContentPath("texture"); } else { if (element.Name.ToString().ToLower() == "vinesprite") { - textureElement = element.Parent.GetAttributeString("vineatlas", ""); + texturePath = element.Parent.GetAttributeContentPath("vineatlas"); } } - if (string.IsNullOrEmpty(textureElement)) { return; } + if (texturePath.IsNullOrEmpty()) { return; } // TODO: parse and create? - if (textureElement.Contains("[GENDER]") || textureElement.Contains("[HEADID]") || textureElement.Contains("[RACE]") || textureElement.Contains("[VARIANT]")) { return; } - if (!textureElement.Contains("/")) + if (texturePath.Value.Contains("[GENDER]") || texturePath.Value.Contains("[HEADID]") || texturePath.Value.Contains("[RACE]") || texturePath.Value.Contains("[VARIANT]")) { return; } + if (!texturePath.Value.Contains("/")) { var parsedPath = element.ParseContentPathFromUri(); spriteFolder = Path.GetDirectoryName(parsedPath); @@ -409,7 +404,7 @@ namespace Barotrauma //{ // loadedSprites.Add(new Sprite(element, spriteFolder)); //} - loadedSprites.Add(new Sprite(element, spriteFolder, textureElement)); + loadedSprites.Add(new Sprite(element, spriteFolder, texturePath.Value, lazyLoad: true)); } } @@ -420,7 +415,7 @@ namespace Barotrauma HashSet docsToSave = new HashSet(); foreach (Sprite sprite in sprites) { - if (sprite.Texture != selectedTexture) { continue; } + if (sprite.FullPath != selectedTexture.FullPath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } element.SetAttributeValue("sourcerect", XMLExtensions.RectToString(sprite.SourceRect)); @@ -448,7 +443,7 @@ namespace Barotrauma doc.SaveSafe(xmlPath); #endif } - xmlPathText.TextColor = GUI.Style.Green; + xmlPathText.TextColor = GUIStyle.Green; return true; } #endregion @@ -478,7 +473,7 @@ namespace Barotrauma { foreach (Sprite sprite in loadedSprites) { - if (sprite.Texture != selectedTexture) continue; + if (sprite.FullPath != selectedTexture.FullPath) { continue; } if (PlayerInput.PrimaryMouseButtonClicked()) { var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom)); @@ -498,7 +493,7 @@ namespace Barotrauma { if (PlayerInput.ScrollWheelSpeed != 0) { - zoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, minZoom, maxZoom); + zoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, MinZoom, MaxZoom); zoomBar.BarScroll = GetBarScrollValue(); } widgets.Values.ForEach(w => w.Update((float)deltaTime)); @@ -645,17 +640,17 @@ namespace Barotrauma if (selectedTexture != null) { textureRect = new Rectangle( - (int)(viewArea.Center.X - selectedTexture.Bounds.Width / 2f * zoom), - (int)(viewArea.Center.Y - selectedTexture.Bounds.Height / 2f * zoom), - (int)(selectedTexture.Bounds.Width * zoom), - (int)(selectedTexture.Bounds.Height * zoom)); + (int)(viewArea.Center.X - selectedTexture.Texture.Bounds.Width / 2f * zoom), + (int)(viewArea.Center.Y - selectedTexture.Texture.Bounds.Height / 2f * zoom), + (int)(selectedTexture.Texture.Bounds.Width * zoom), + (int)(selectedTexture.Texture.Bounds.Height * zoom)); - spriteBatch.Draw(selectedTexture, + spriteBatch.Draw(selectedTexture.Texture, viewArea.Center.ToVector2(), sourceRectangle: null, color: Color.White, rotation: 0.0f, - origin: new Vector2(selectedTexture.Bounds.Width / 2.0f, selectedTexture.Bounds.Height / 2.0f), + origin: new Vector2(selectedTexture.Texture.Bounds.Width / 2.0f, selectedTexture.Texture.Bounds.Height / 2.0f), scale: zoom, effects: SpriteEffects.None, layerDepth: 0); @@ -670,10 +665,8 @@ namespace Barotrauma foreach (GUIComponent element in spriteList.Content.Children) { - Sprite sprite = element.UserData as Sprite; - if (sprite == null) { continue; } - if (sprite.Texture != selectedTexture) continue; - spriteCount++; + if (!(element.UserData is Sprite sprite)) { continue; } + if (sprite.FullPath != selectedTexture.FullPath) { continue; } Rectangle sourceRect = new Rectangle( textureRect.X + (int)(sprite.SourceRect.X * zoom), @@ -682,10 +675,10 @@ namespace Barotrauma (int)(sprite.SourceRect.Height * zoom)); bool isSelected = selectedSprites.Contains(sprite); - GUI.DrawRectangle(spriteBatch, sourceRect, isSelected ? GUI.Style.Orange : GUI.Style.Red * 0.5f, thickness: isSelected ? 2 : 1); + GUI.DrawRectangle(spriteBatch, sourceRect, isSelected ? GUIStyle.Orange : GUIStyle.Red * 0.5f, thickness: isSelected ? 2 : 1); - string id = sprite.ID; - if (!string.IsNullOrEmpty(id)) + Identifier id = sprite.Identifier; + if (!id.IsEmpty) { int widgetSize = 10; Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2(); @@ -759,8 +752,6 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); - spriteCount = 0; - spriteBatch.End(); } @@ -829,7 +820,7 @@ namespace Barotrauma foreach (GUIComponent child in textureList.Content.Children) { if (!(child is GUITextBlock textBlock)) { continue; } - textBlock.Visible = textBlock.Text.ToLower().Contains(text); + textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase); } } private void FilterSprites(string text) @@ -845,7 +836,7 @@ namespace Barotrauma foreach (GUIComponent child in spriteList.Content.Children) { if (!(child is GUITextBlock textBlock)) { continue; } - textBlock.Visible = textBlock.Text.ToLower().Contains(text); + textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase); } } @@ -854,25 +845,7 @@ namespace Barotrauma base.Select(); LoadSprites(); RefreshLists(); - // Store the reference, because lastSelected is reassigned when the texture is selected. - Sprite lastSprite = lastSelected; - // Select the last selected texture if any. - // TODO: Does not work if the texture has been disposed. This happens when it's not used by any sprite -> is there a better way to identify the textures? id or something? - if (selectedTexture != null && textureList.Content.Children.Any(c => c.UserData as Texture2D == selectedTexture)) - { - textureList.Select(selectedTexture, autoScroll: false); - UpdateScrollBar(textureList); - // Select the last selected sprite if any - if (lastSprite != null && spriteList.Content.Children.FirstOrDefault(c => c.UserData is Sprite s && s.ID == lastSprite.ID)?.UserData is Sprite sprite) - { - spriteList.Select(sprite, autoScroll: false); - UpdateScrollBar(spriteList); - } - } - else - { - spriteList.Select(0, autoScroll: false); - } + spriteList.Select(0, autoScroll: false); } public override void Deselect() @@ -887,7 +860,7 @@ namespace Barotrauma { foreach (var s in Sprite.LoadedSprites) { - if (s.Texture == sprite.Texture && !reloadedSprites.Contains(s)) + if (s.FullPath == sprite.FullPath && !reloadedSprites.Contains(s)) { s.ReloadXML(); reloadedSprites.Add(s); @@ -907,7 +880,7 @@ namespace Barotrauma RefreshLists(); } - if (selectedSprites.Any(s => s.Texture != selectedTexture)) + if (selectedSprites.Any(s => s.FullPath != selectedTexture.FullPath)) { ResetWidgets(); } @@ -921,7 +894,6 @@ namespace Barotrauma { selectedSprites.Add(sprite); dirtySprites.Add(sprite); - lastSelected = sprite; } } else @@ -929,9 +901,8 @@ namespace Barotrauma selectedSprites.Clear(); selectedSprites.Add(sprite); dirtySprites.Add(sprite); - lastSelected = sprite; } - if (selectedTexture != sprite.Texture) + if (selectedTexture?.FullPath != sprite.FullPath) { textureList.Select(sprite.Texture, autoScroll: false); UpdateScrollBar(textureList); @@ -939,7 +910,7 @@ namespace Barotrauma xmlPathText.Text = string.Empty; foreach (var s in selectedSprites) { - texturePathText.Text = s.FilePath; + texturePathText.Text = s.FilePath.Value; var element = s.SourceElement; if (element != null) { @@ -962,18 +933,18 @@ namespace Barotrauma ResetWidgets(); HashSet textures = new HashSet(); // Create texture list - foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath))) + foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath.Value))) { //ignore sprites that don't have a file path (e.g. submarine pics) - if (string.IsNullOrEmpty(sprite.FilePath)) continue; - string normalizedFilePath = Path.GetFullPath(sprite.FilePath); + if (sprite.FilePath.IsNullOrEmpty()) continue; + string normalizedFilePath = sprite.FilePath.FullPath; if (!textures.Contains(normalizedFilePath)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textureList.Content.RectTransform) { MinSize = new Point(0, 20) }, - Path.GetFileName(sprite.FilePath)) + Path.GetFileName(sprite.FilePath.Value)) { - ToolTip = sprite.FilePath, - UserData = sprite.Texture + ToolTip = sprite.FilePath.Value, + UserData = sprite }; textures.Add(normalizedFilePath); } @@ -981,7 +952,7 @@ namespace Barotrauma // Create sprite list // TODO: allow the user to choose whether to sort by file name or by texture sheet //foreach (Sprite sprite in loadedSprites.OrderBy(s => GetSpriteName(s))) - foreach (Sprite sprite in loadedSprites.OrderBy(s => s.SourceElement.GetAttributeString("texture", string.Empty))) + foreach (Sprite sprite in loadedSprites.OrderBy(s => s.SourceElement.GetAttributeContentPath("texture")?.Value ?? string.Empty)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), spriteList.Content.RectTransform) { MinSize = new Point(0, 20) }, GetSpriteName(sprite) + " (" + sprite.SourceRect.X + ", " + sprite.SourceRect.Y + ", " + sprite.SourceRect.Width + ", " + sprite.SourceRect.Height + ")") @@ -996,9 +967,8 @@ namespace Barotrauma { if (selectedTexture == null) { return; } var viewArea = GetViewArea; - float width = viewArea.Width / (float)selectedTexture.Width; - float height = viewArea.Height / (float)selectedTexture.Height; - maxZoom = 10; // TODO: user-definable? + float width = viewArea.Width / (float)selectedTexture.Texture.Width; + float height = viewArea.Height / (float)selectedTexture.Texture.Height; zoom = Math.Min(1, Math.Min(width, height)); zoomBar.BarScroll = GetBarScrollValue(); viewAreaOffset = Point.Zero; @@ -1017,7 +987,7 @@ namespace Barotrauma } } - private float GetBarScrollValue() => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(minZoom, maxZoom, zoom)); + private float GetBarScrollValue() => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(MinZoom, MaxZoom, zoom)); private string GetSpriteName(Sprite sprite) { @@ -1032,7 +1002,7 @@ namespace Barotrauma { name = sourceElement.Parent.GetAttributeString("name", string.Empty); } - return string.IsNullOrEmpty(name) ? Path.GetFileNameWithoutExtension(sprite.FilePath) : name; + return string.IsNullOrEmpty(name) ? Path.GetFileNameWithoutExtension(sprite.FilePath.Value) : name; } private void UpdateScrollBar(GUIListBox listBox) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs deleted file mode 100644 index 60cdf08a0..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ /dev/null @@ -1,1923 +0,0 @@ -using Barotrauma.IO; -using Barotrauma.Steam; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Barotrauma -{ - class SteamWorkshopScreen : Screen - { - private GUIFrame menu; - private GUIListBox subscribedItemList, topItemList; - private GUITextBox subscribedItemFilter, topItemFilter; - - private GUIListBox publishedItemList, myItemList; - - //shows information of a selected workshop item - private GUIFrame modsPreviewFrame, browsePreviewFrame; - - //menu for creating new items - private GUIFrame createItemFrame; - //listbox that shows the files included in the item being created - private GUIListBox createItemFileList; - - private GUIImage previewIcon; - - private System.IO.FileSystemWatcher createItemWatcher; - - private readonly List tabButtons = new List(); - - private class PendingPreviewImageDownload - { - /// - /// Was the image downloaded - /// - public bool Downloaded = false; - - /// - /// How many tasks are looking to create a preview image based on this download - /// - public int PendingLoads = 1; - } - private readonly Dictionary pendingPreviewImageDownloads = new Dictionary(); - private readonly Dictionary itemPreviewSprites = new Dictionary(); - - private enum Tab - { - Mods, - Browse, - Publish - } - - private GUIComponent[] tabs; - - private ContentPackage itemContentPackage; - private Steamworks.Ugc.Editor? itemEditor; - - private enum VisibilityType - { - Public, - FriendsOnly, - Private - } - - public SteamWorkshopScreen() - { - GameMain.Instance.ResolutionChanged += CreateUI; - CreateUI(); - - Steamworks.SteamUGC.GlobalOnItemInstalled += OnItemInstalled; - } - - private void CreateUI() - { - tabs = new GUIComponent[Enum.GetValues(typeof(Tab)).Length]; - menu = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); - - var container = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), menu.RectTransform, Anchor.Center)) { Stretch = true }; - var topButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), container.RectTransform), isHorizontal: true); - - foreach (Tab tab in Enum.GetValues(typeof(Tab))) - { - GUIButton tabButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), topButtonContainer.RectTransform), - TextManager.Get(tab.ToString() + "Tab"), style: "GUITabButton") - { - UserData = tab, - OnClicked = (btn, userData) => - { - SelectTab((Tab)userData); return true; - } - }; - tabButtons.Add(tabButton); - } - topButtonContainer.RectTransform.MinSize = new Point(0, topButtonContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - topButtonContainer.RectTransform.MaxSize = new Point(int.MaxValue, topButtonContainer.RectTransform.MinSize.Y); - - var tabContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.7f), container.RectTransform), style: "InnerFrame"); - - var bottomButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), container.RectTransform), isHorizontal: true); - GUIButton backButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.9f), bottomButtonContainer.RectTransform) { MinSize = new Point(150, 0) }, - TextManager.Get("Back")) - { - OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu - }; - backButton.SelectedColor = backButton.Color; - topButtonContainer.RectTransform.MinSize = new Point(0, backButton.RectTransform.MinSize.Y); - topButtonContainer.RectTransform.MaxSize = new Point(int.MaxValue, backButton.RectTransform.MinSize.Y); - - //------------------------------------------------------------------------------- - //Subscribed Mods tab - //------------------------------------------------------------------------------- - - tabs[(int)Tab.Mods] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var modsContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Mods].RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - subscribedItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), modsContainer.RectTransform)) - { - ScrollBarVisible = true, - OnSelected = (GUIComponent component, object userdata) => - { - if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } - ShowItemPreview(userdata as Steamworks.Ugc.Item?, modsPreviewFrame); - return true; - } - }; - - subscribedItemFilter = CreateFilterBox(modsContainer, subscribedItemList); - - modsPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Mods].RectTransform, Anchor.TopRight), style: null); - - //------------------------------------------------------------------------------- - //Popular Mods tab - //------------------------------------------------------------------------------- - - tabs[(int)Tab.Browse] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var listContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Browse].RectTransform), childAnchor: Anchor.TopCenter) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - topItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.9f), listContainer.RectTransform)) - { - ScrollBarVisible = true, - OnSelected = (GUIComponent component, object userdata) => - { - ShowItemPreview(userdata as Steamworks.Ugc.Item?, browsePreviewFrame); - return true; - } - }; - - topItemFilter = CreateFilterBox(listContainer, topItemList); - - new GUIButton(new RectTransform(new Vector2(1.0f, 0.02f), listContainer.RectTransform), TextManager.Get("FindModsButton"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - SteamManager.OverlayCustomURL("steam://url/SteamWorkshopPage/" + SteamManager.AppID); - return true; - } - }; - - browsePreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Browse].RectTransform, Anchor.TopRight), style: null); - - //------------------------------------------------------------------------------- - //Publish tab - //------------------------------------------------------------------------------- - - tabs[(int)Tab.Publish] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Publish].RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("PublishedWorkshopItems"), font: GUI.SubHeadingFont); - publishedItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), leftColumn.RectTransform)) - { - OnSelected = (component, userdata) => - { - if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } - if (GUI.MouseOn is GUITickBox || GUI.MouseOn?.Parent is GUITickBox) { return false; } - myItemList.Deselect(); - if (userdata is Steamworks.Ugc.Item?) - { - var item = userdata as Steamworks.Ugc.Item?; - if (!(item?.IsInstalled ?? false)) { return false; } - if (CreateWorkshopItem(item)) { ShowCreateItemFrame(); } - } - return true; - } - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("YourWorkshopItems"), font: GUI.SubHeadingFont); - myItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), leftColumn.RectTransform)) - { - OnSelected = (component, userdata) => - { - if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } - publishedItemList.Deselect(); - if (userdata is SubmarineInfo sub) - { - CreateWorkshopItem(sub); - } - else if (userdata is ContentPackage contentPackage) - { - CreateWorkshopItem(contentPackage); - } - ShowCreateItemFrame(); - return true; - } - }; - - createItemFrame = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), tabs[(int)Tab.Publish].RectTransform, Anchor.TopRight), style: null); - - SelectTab(Tab.Mods); - - CoroutineManager.StartCoroutine(PollSubscribedItems()); - } - - private GUITextBox CreateFilterBox(GUIComponent parent, GUIListBox listbox) - { - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) - { - Stretch = true - }; - filterContainer.RectTransform.SetAsFirstChild(); - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => - { - foreach (GUIComponent child in listbox.Content.Children) - { - if (!(child.UserData is Steamworks.Ugc.Item item)) { continue; } - child.Visible = string.IsNullOrEmpty(text) ? true : (item.Title?.ToLower().Contains(text.ToLower()) ?? false); - } - return true; - }; - - return searchBox; - } - - public override void Select() - { - base.Select(); - - modsPreviewFrame.ClearChildren(); - browsePreviewFrame.ClearChildren(); - createItemFrame.ClearChildren(); - itemContentPackage = null; - itemEditor = null; - - SelectTab(Tab.Mods); - } - - public override void OnFileDropped(string filePath, string extension) - { - switch (extension) - { - case ".png": // workshop preview - case ".jpg": - case ".jpeg": - if (previewIcon == null || itemContentPackage == null) { break; } - - OnPreviewImageSelected(previewIcon, filePath); - break; - - default: - DebugConsole.ThrowError($"Could not drag and drop the file. \"{extension}\" is not a valid file extension! (expected .png, .jpg or .jpeg)"); - break; - } - } - - private void OnItemInstalled(ulong itemId) - { - RefreshSubscribedItems(); - } - - float subscribePollAdditionalWait = 0.0f; - - private IEnumerable PollSubscribedItems() - { - if (!SteamManager.IsInitialized) { yield return CoroutineStatus.Success; } - - uint numSubscribed = 0; - while (true) - { - while (CoroutineManager.IsCoroutineRunning("Load")) { yield return new WaitForSeconds(1.0f); } - while (subscribePollAdditionalWait > 0.01f) - { - subscribePollAdditionalWait = Math.Min(subscribePollAdditionalWait, 3.0f); - float wait = subscribePollAdditionalWait; - yield return new WaitForSeconds(wait); - subscribePollAdditionalWait -= wait; - } - uint newNumSubscribed = Steamworks.SteamUGC.NumSubscribedItems; - if (newNumSubscribed != numSubscribed) - { - RefreshSubscribedItems(); - numSubscribed = newNumSubscribed; - } - - yield return new WaitForSeconds(1.0f); - } - } - - private void SelectTab(Tab tab) - { - for (int i = 0; i < tabs.Length; i++) - { - tabButtons[i].Selected = tabs[i].Visible = i == (int)tab; - } - - if (createItemFrame.CountChildren == 0) - { - new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.9f), createItemFrame.RectTransform, Anchor.Center), - TextManager.Get("WorkshopItemCreateHelpText"), wrap: true) - { - CanBeFocused = false - }; - } - - createItemWatcher?.Dispose(); createItemWatcher = null; - if (Screen.Selected == this) - { - switch (tab) - { - case Tab.Mods: - RefreshSubscribedItems(); - break; - case Tab.Browse: - RefreshPopularItems(); - break; - case Tab.Publish: - RefreshPublishedItems(); - break; - } - } - } - - public IEnumerable RefreshDownloadState() - { - bool isDownloading = true; - while (true) - { - SteamManager.GetSubscribedWorkshopItems((items) => - { - isDownloading = items.Any(it => it.IsDownloading || it.IsDownloadPending); - - GameMain.MainMenuScreen.SetDownloadingModsNotification(isDownloading); - }); - - if (!isDownloading) { break; } - - yield return new WaitForSeconds(0.5f); - } - yield return CoroutineStatus.Success; - } - - private void RefreshSubscribedItems() - { - SteamManager.GetSubscribedWorkshopItems((items) => - { - //filter out the items published by the player (they're shown in the publish tab) - var mySteamID = SteamManager.GetSteamID(); - OnItemsReceived(GetVisibleItems(items.Where(it => it.Owner.Id != mySteamID)), subscribedItemList); - - GameMain.MainMenuScreen.SetDownloadingModsNotification(items.Any(it => it.IsDownloading || it.IsDownloadPending)); - }); - } - - private void RefreshPopularItems() - { - SteamManager.GetPopularWorkshopItems((items) => { OnItemsReceived(GetVisibleItems(items), topItemList); }, 20); - } - - private void RefreshPublishedItems() - { - SteamManager.GetPublishedWorkshopItems((items) => { OnItemsReceived(items, publishedItemList); }); - RefreshMyItemList(); - } - - private IEnumerable GetVisibleItems(IEnumerable items) - { -#if UNSTABLE - //show everything in Unstable - return items; -#else - //hide Unstable items in normal version - return items.Where(it => !it.HasTag("unstable")); -#endif - } - - private void RefreshMyItemList() - { - myItemList.ClearChildren(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), myItemList.Content.RectTransform), TextManager.Get("WorkshopLabelSubmarines"), - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - CanBeFocused = false - }; - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) - { - if (sub.HasTag(SubmarineTag.HideInMenus)) { continue; } - string subPath = Path.GetFullPath(sub.FilePath); - - //ignore files that are part of the vanilla content package - if (GameMain.VanillaContent != null && - GameMain.VanillaContent.Files.Any(s => Path.GetFullPath(s.Path) == subPath)) - { - continue; - } - //ignore subs that are part of a workshop content package - if (ContentPackage.AllPackages.Any(cp => cp.SteamWorkshopId != 0 && - cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) - { - continue; - } - //ignore subs that are defined in a content package with more files than just the sub - //(these will be listed in the "content packages" section) - if (ContentPackage.AllPackages.Any(cp => cp.Files.Count > 1 && - cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) - { - continue; - } - - CreateMyItemFrame(sub, myItemList); - } - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), myItemList.Content.RectTransform), TextManager.Get("WorkshopLabelContentPackages"), - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - CanBeFocused = false - }; - foreach (ContentPackage contentPackage in ContentPackage.AllPackages) - { - if (contentPackage.SteamWorkshopId != 0 || contentPackage.HideInWorkshopMenu) { continue; } - if (contentPackage == GameMain.VanillaContent) { continue; } - //don't list content packages that only define one sub (they're visible in the "Submarines" section) - if (contentPackage.Files.Count == 1 && contentPackage.Files[0].Type == ContentType.Submarine) { continue; } - CreateMyItemFrame(contentPackage, myItemList); - } - } - - private void OnItemsReceived(IEnumerable itemDetails, GUIListBox listBox) - { - CrossThread.RequestExecutionOnMainThread(() => - { - listBox.ClearChildren(); - foreach (var item in itemDetails) - { - CreateWorkshopItemFrame(item, listBox); - } - - if (itemDetails.Count() == 0 && listBox == subscribedItemList) - { - new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.9f), listBox.Content.RectTransform, Anchor.Center), TextManager.Get("NoSubscribedMods"), wrap: true) - { - CanBeFocused = false - }; - } - }); - } - - private void CreateWorkshopItemFrame(Steamworks.Ugc.Item? item, GUIListBox listBox) - { - if (string.IsNullOrEmpty(item?.Title)) - { - return; - } - - string text = string.Empty; - if (listBox == subscribedItemList) - { - text = subscribedItemFilter.Text; - } - else if (listBox == topItemList) - { - text = topItemFilter.Text; - } - - bool visible = string.IsNullOrEmpty(text) || (item?.Title?.ToLower().Contains(text.ToLower()) ?? false); - - int prevIndex = -1; - var existingFrame = listBox.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); - if (existingFrame != null) - { - prevIndex = listBox.Content.GetChildIndex(existingFrame); - listBox.Content.RemoveChild(existingFrame); - } - - var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), - style: "ListBoxElement") - { - UserData = item, - Visible = visible - }; - if (prevIndex > -1) - { - itemFrame.RectTransform.RepositionChildInHierarchy(prevIndex); - } - - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) - { - CanBeFocused = false, - Stretch = true - }; - - int iconSize = innerFrame.Rect.Height; - if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) - { - new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), itemPreviewSprites[item?.PreviewImageUrl], scaleToFit: true) - { - UserData = "previewimage", - CanBeFocused = false - }; - } - else if (Screen.Selected == this) - { - new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true) - { - UserData = "previewimage", - CanBeFocused = false - }; - try - { - if (!string.IsNullOrEmpty(item?.PreviewImageUrl)) - { - string imagePreviewPath = Path.Combine(SteamManager.WorkshopItemPreviewImageFolder, item?.Id + ".png"); - - bool isNewImage; - lock (pendingPreviewImageDownloads) - { - isNewImage = !pendingPreviewImageDownloads.ContainsKey(item.Value.Id); - if (isNewImage) - { - if (File.Exists(imagePreviewPath)) - { - File.Delete(imagePreviewPath); - } - - pendingPreviewImageDownloads.Add(item.Value.Id, new PendingPreviewImageDownload()); - } - } - - if (isNewImage) - { - Directory.CreateDirectory(SteamManager.WorkshopItemPreviewImageFolder); - - Uri baseAddress = new Uri(item?.PreviewImageUrl); - Uri directory = new Uri(baseAddress, "."); // "." == current dir, like MS-DOS - string fileName = Path.GetFileName(baseAddress.LocalPath); - - IRestClient client = new RestClient(directory); - var request = new RestRequest(fileName, Method.GET); - client.ExecuteAsync(request, response => - { - OnPreviewImageDownloaded(response, imagePreviewPath, - () => - { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads[item.Value.Id].Downloaded = true; - } - CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); - }); - }); - } - else - { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads[item.Value.Id].PendingLoads++; - } - CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); - } - } - } - catch (Exception e) - { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads.Remove(item.Value.Id); - } - DebugConsole.ThrowError("Downloading the preview image of the Workshop item \"" + item?.Title + "\" failed.", e); - } - } - - var rightColumn = new GUILayoutGroup(new RectTransform(new Point(innerFrame.Rect.Width - iconSize, innerFrame.Rect.Height), innerFrame.RectTransform), childAnchor: Anchor.CenterLeft) - { - IsHorizontal = true, - Stretch = true, - RelativeSpacing = 0.05f, - CanBeFocused = false - }; - - var titleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), rightColumn.RectTransform), item?.Title, textAlignment: Alignment.CenterLeft, wrap: true) - { - UserData = "titletext", - CanBeFocused = false - }; - - if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false) && Directory.Exists(item?.Directory)) - { - bool installed = SteamManager.CheckWorkshopItemInstalled(item); - - if (!installed) - { - bool? compatible = SteamManager.CheckWorkshopItemCompatibility(item); - if (compatible.HasValue && !compatible.Value) - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.3f), rightColumn.RectTransform), - TextManager.Get("WorkshopItemIncompatible"), textColor: GUI.Style.Red) - { - ToolTip = TextManager.Get("WorkshopItemIncompatibleTooltip") - }; - } - else - { - installed = SteamManager.InstallWorkshopItem(item, out string errorMsg, Selected == this); - if (!installed) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - titleText.TextColor = Color.Red; - titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); - } - } - } - - if (installed) - { - bool upToDate = SteamManager.CheckWorkshopItemUpToDate(item); - - if (!upToDate) - { - if (!SteamManager.UpdateWorkshopItem(item, out string errorMsg)) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - titleText.TextColor = Color.Red; - titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); - } - } - } - - } - else if (item?.IsDownloading ?? false) - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloading")); - } - else if (item?.IsDownloadPending ?? false) - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloadPending")); - } - else if (!(item?.IsSubscribed ?? false) && (listBox != subscribedItemList)) - { - var downloadBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIPlusButton") - { - ToolTip = TextManager.Get("DownloadButton"), - ForceUpperCase = true, - UserData = item - }; - downloadBtn.OnClicked = (btn, userdata) => { DownloadItem(itemFrame, downloadBtn, item); return true; }; - } - - if ((item?.IsSubscribed ?? false) && listBox == subscribedItemList) - { - var reinstallBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIReloadButton") - { - ToolTip = TextManager.Get("WorkshopItemReinstall"), - ForceUpperCase = true, - UserData = "reinstall" - }; - reinstallBtn.OnClicked = (btn, userdata) => - { - var elem = subscribedItemList.Content.GetChildByUserData(item); - try - { - bool reselect = GameMain.Config.AllEnabledPackages.Any(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == item?.Id); - if (!SteamManager.UninstallWorkshopItem(item, false, out string errorMsg)) - { - DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); - elem.Flash(GUI.Style.Red); - return true; - } - - SteamManager.ForceRedownload(item?.Id ?? 0, () => - { - if (!SteamManager.InstallWorkshopItem(item, out string errorMsg, reselect, true)) - { - DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); - elem.Flash(GUI.Style.Red); - } - RefreshSubscribedItems(); - }); - RefreshSubscribedItems(); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\"", e, true); - elem.Flash(GUI.Style.Red); - } - return true; - }; - reinstallBtn.Enabled = !item.Value.IsDownloading && !item.Value.IsDownloadPending; - var unsubBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIMinusButton") - { - ToolTip = TextManager.Get("WorkshopItemUnsubscribe"), - ForceUpperCase = true, - UserData = "unsubscribe" - }; - unsubBtn.OnClicked = (btn, userdata) => - { - subscribePollAdditionalWait += 1.0f; - item?.Unsubscribe(); - SteamManager.UninstallWorkshopItem(item, true, out _); - subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); - return true; - }; - } - - innerFrame.Recalculate(); - listBox.RecalculateChildren(); - } - - public void SetReinstallButtonStatus(Steamworks.Ugc.Item? item, bool enabled, Color? flashColor) - { - var child = subscribedItemList.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); - if (child != null) - { - var reinstallBtn = child.FindChild("reinstall", true); - if (reinstallBtn != null) { reinstallBtn.Enabled = enabled; } - var unsubBtn = child.FindChild("unsubscribe", true); - if (unsubBtn != null) { unsubBtn.Enabled = enabled; } - if (flashColor.HasValue) { child.Flash(flashColor); } - } - } - - private void RemoveItemFromLists(ulong itemID) - { - RemoveItemFromList(publishedItemList); - RemoveItemFromList(subscribedItemList); - RemoveItemFromList(topItemList); - - void RemoveItemFromList(GUIListBox listBox) - { - listBox.Content.RemoveChild( - listBox.Content.Children.FirstOrDefault(c => c.UserData is Steamworks.Ugc.Item? && (c.UserData as Steamworks.Ugc.Item?)?.Id == itemID)); - } - } - - private void CreateMyItemFrame(SubmarineInfo submarine, GUIListBox listBox) - { - var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), - style: "ListBoxElement") - { - UserData = submarine - }; - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.1f, - Stretch = true - }; - if (submarine.PreviewImage != null) - { - new GUIImage(new RectTransform(new Point(innerFrame.Rect.Height), innerFrame.RectTransform), submarine.PreviewImage, scaleToFit: true); - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), submarine.Name, textAlignment: Alignment.CenterLeft); - } - private void CreateMyItemFrame(ContentPackage contentPackage, GUIListBox listBox) - { - var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), - style: "ListBoxElement") - { - UserData = contentPackage - }; - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.1f, - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), contentPackage.Name, textAlignment: Alignment.CenterLeft); - } - - private void OnPreviewImageDownloaded(IRestResponse response, string previewImagePath, Action action) - { - if (response.ResponseStatus == ResponseStatus.Completed) - { - TaskPool.Add("WritePreviewImageAsync", WritePreviewImageAsync(response, previewImagePath), (task) => { action?.Invoke(); }); - } - } - - private async Task WritePreviewImageAsync(IRestResponse response, string previewImagePath) - { - await Task.Yield(); - try - { - File.WriteAllBytes(previewImagePath, response.RawBytes); - } - catch (Exception e) - { - GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsManager.ErrorSeverity.Error, "Failed to save workshop item preview image.\n" + e.Message); - return; - } - } - - private IEnumerable WaitForItemPreviewDownloaded(Steamworks.Ugc.Item? item, GUIListBox listBox, string previewImagePath) - { - while (true) - { - lock (pendingPreviewImageDownloads) - { - if (pendingPreviewImageDownloads[item.Value.Id].Downloaded){ break; } - } - - yield return new WaitForSeconds(0.2f); - } - - if (File.Exists(previewImagePath)) - { - TaskPool.Add("LoadPreviewImageAsync", LoadPreviewImageAsync(item?.PreviewImageUrl, previewImagePath), - new Tuple(item, listBox), - (task, tuple) => - { - //must be done in the main thread because creating/removing GUI elements is not thread-safe - CrossThread.RequestExecutionOnMainThread(() => - { - (var it, var lb) = tuple; - if (lb.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) - { - if (task.TryGetResult(out Sprite sprite)) { previewImage.Sprite = sprite; } - } - else - { - CreateWorkshopItemFrame(it, lb); - } - - if (modsPreviewFrame.FindChild(it) != null) - { - ShowItemPreview(it, modsPreviewFrame); - } - if (browsePreviewFrame.FindChild(item) != null) - { - ShowItemPreview(it, browsePreviewFrame); - } - - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads[it.Value.Id].PendingLoads--; - if (pendingPreviewImageDownloads[it.Value.Id].PendingLoads <= 0) { pendingPreviewImageDownloads.Remove(it.Value.Id); } - } - }); - }); - } - - yield return CoroutineStatus.Success; - } - - private async Task LoadPreviewImageAsync(string previewImageUrl, string previewImagePath) - { - await Task.Yield(); - lock (itemPreviewSprites) - { - if (itemPreviewSprites.ContainsKey(previewImageUrl)) - { - return itemPreviewSprites[previewImageUrl]; - } - else - { - Sprite newSprite = new Sprite(previewImagePath, sourceRectangle: null); - itemPreviewSprites.Add(previewImageUrl, newSprite); - return newSprite; - } - } - } - - private bool DownloadItem(GUIComponent frame, GUIButton downloadButton, Steamworks.Ugc.Item? item) - { - if (item == null) { return false; } - - var parentElement = downloadButton.Parent; - parentElement.RemoveChild(downloadButton); - var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), parentElement.RectTransform), TextManager.Get("WorkshopItemDownloading")); - - SteamManager.SubscribeToWorkshopItem(item.Value.Id, () => - { - if (SteamManager.InstallWorkshopItem(item, out _)) - { - textBlock.Text = TextManager.Get("workshopiteminstalled"); - frame.Flash(GUI.Style.Green); - } - else - { - frame.Flash(GUI.Style.Red); - } - RefreshSubscribedItems(); - }); - - return true; - } - - private void ShowItemPreview(Steamworks.Ugc.Item? item, GUIFrame itemPreviewFrame) - { - itemPreviewFrame.ClearChildren(); - - if (item == null) { return; } - - var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), itemPreviewFrame.RectTransform, Anchor.Center)) - { - Stretch = true, - UserData = item - }; - - var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), content.RectTransform)) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerArea.RectTransform), item?.Title, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont, wrap: true); - - new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.0f), headerArea.RectTransform), item?.Owner.Name, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); - - var btn = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), headerArea.RectTransform, Anchor.CenterRight), TextManager.Get("WorkshopShowItemInSteam"), style: "GUIButtonSmall") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => - { - SteamManager.OverlayCustomURL("steam://url/CommunityFilePage/" + item?.Id); - return true; - } - }; - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.005f), content.RectTransform), style: "HorizontalLine"); - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); - - //--------------- - - var centerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) - { - new GUIImage(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform), itemPreviewSprites[item?.PreviewImageUrl], scaleToFit: true); - } - else - { - new GUIImage(new RectTransform(new Vector2(0.5f, 0.0f), centerArea.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); - } - - var statsFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform), style: "GUIFrameListBox"); - var statsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), statsFrame.RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - //score ------------------------------------- - var scoreContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.0f), scoreContainer.RectTransform), TextManager.Get("WorkshopItemScore"), font: GUI.SubHeadingFont); - int starCount = (int)Math.Round((item?.Score ?? 0.0f) * 5); - for (int i = 0; i < 5; i++) - { - new GUIImage(new RectTransform(new Point(scoreContainer.Rect.Height), scoreContainer.RectTransform), - i < starCount ? "GUIStarIconBright" : "GUIStarIconDark"); - } - new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.0f), scoreContainer.RectTransform), - TextManager.GetWithVariable("WorkshopItemVotes", "[votecount]", (item.Value.VotesUp + item.Value.VotesDown).ToString()), - textAlignment: Alignment.CenterRight); - - //tags ------------------------------------ - - List tags = new List(); - for (int i = 0; i < item?.Tags.Length && i < 5; i++) - { - if (string.IsNullOrEmpty(item?.Tags[i])) { continue; } - string tag = TextManager.Get("Workshop.ContentTag." + item?.Tags[i].Replace(" ", ""), true); - if (string.IsNullOrEmpty(tag)) { tag = item?.Tags[i].CapitaliseFirstInvariant(); } - tags.Add(tag); - } - if (tags.Count > 0) - { - var tagContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f, - CanBeFocused = true - }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), tagContainer.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUI.SubHeadingFont); - - var t = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), tagContainer.RectTransform, Anchor.TopRight), string.Join(", ", tags), textAlignment: Alignment.CenterRight); - t.RectTransform.SizeChanged += () => - { - t.TextScale = 1.0f; - t.AutoScaleHorizontal = true; - }; - } - - var fileSize = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemFileSize"), font: GUI.SubHeadingFont); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), fileSize.RectTransform, Anchor.TopRight), MathUtils.GetBytesReadable(item?.IsInstalled ?? false ? (long)item.Value.SizeBytes : item.Value.DownloadBytesDownloaded), textAlignment: Alignment.CenterRight); - - //var dateContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), isHorizontal: true); - - var creationDate = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemCreationDate"), font: GUI.SubHeadingFont); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), creationDate.RectTransform, Anchor.CenterRight), item?.Created.ToString("dd.MM.yyyy"), textAlignment: Alignment.CenterRight); - - var modificationDate = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemModificationDate"), font: GUI.SubHeadingFont); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), modificationDate.RectTransform, Anchor.CenterRight), item?.Updated.ToString("dd.MM.yyyy"), textAlignment: Alignment.CenterRight); - - if (item?.IsSubscribed ?? false) - { - var buttonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), style: null); - var unsubscribeButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.95f), buttonContainer.RectTransform, Anchor.Center), TextManager.Get("WorkshopItemUnsubscribe"), style: "GUIButtonSmall") - { - UserData = item, - OnClicked = (btn, userdata) => - { - subscribePollAdditionalWait += 1.0f; - item?.Unsubscribe(); - SteamManager.UninstallWorkshopItem(item, true, out _); - subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); - itemPreviewFrame.ClearChildren(); - return true; - } - }; - buttonContainer.RectTransform.MinSize = unsubscribeButton.RectTransform.MinSize; - statsContent.Recalculate(); - } - - //------------------ - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); - - var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)) { ScrollBarVisible = true }; - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform) { MinSize = new Point(0, 5) }, style: null); - - string description = item?.Description; - description = ToolBox.RemoveBBCodeTags(description); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform), description, wrap: true) - { - CanBeFocused = false - }; - } - - private void CreateWorkshopItem(SubmarineInfo sub) - { - string destinationFolder = Path.Combine("Mods", sub.Name.Trim()); - itemContentPackage = ContentPackage.CreatePackage(sub.Name, Path.Combine(destinationFolder, SteamManager.MetadataFileName), corePackage: false); - SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); - - bool fileMoved = false; - string submarineDir = Path.GetDirectoryName(sub.FilePath); - if (submarineDir != Path.GetDirectoryName(destinationFolder)) - { - string destinationPath = Path.Combine(destinationFolder, Path.GetFileName(sub.FilePath)); - if (!File.Exists(destinationPath)) - { - File.Move(sub.FilePath, destinationPath); - } - fileMoved = true; - sub.FilePath = destinationPath; - } - - itemContentPackage.AddFile(sub.FilePath, ContentType.Submarine); - itemContentPackage.Name = sub.Name; - itemContentPackage.Save(itemContentPackage.Path); - - if (fileMoved) - { - GameMain.Config.EnableRegularPackage(itemContentPackage); - } - - itemEditor = itemEditor?.WithTitle(sub.Name).WithTag("Submarine").WithDescription(sub.Description); - - if (sub.PreviewImage != null) - { - string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); - try - { - using (System.IO.Stream s = File.Create(previewImagePath)) - { - sub.PreviewImage.Texture.SaveAsPng(s, (int)sub.PreviewImage.size.X, (int)sub.PreviewImage.size.Y); - itemEditor = itemEditor?.WithPreviewFile(previewImagePath); - } - if (new FileInfo(previewImagePath).Length > 1024 * 1024) - { - new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); - itemEditor = itemEditor?.WithPreviewFile(SteamManager.DefaultPreviewImagePath); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving submarine preview image failed.", e); - itemEditor = itemEditor?.WithPreviewFile(null); - } - } - } - private void CreateWorkshopItem(ContentPackage contentPackage) - { - //SteamManager.CreateWorkshopItemStaging(new List(), out itemEditor, out itemContentPackage); - - itemContentPackage = contentPackage; - SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); - itemEditor = itemEditor?.WithTitle(contentPackage.Name); - - /*string modDirectory = ""; - foreach (ContentFile file in contentPackage.Files) - { - itemContentPackage.AddFile(file.Path, file.Type); - //if some of the content files are in a subdirectory of the Mods folder, - //assume that directory contains mod files for this package and copy them to the staging folder - if (modDirectory == "" && ContentPackage.IsModFilePathAllowed(file.Path)) - { - string directoryName = Path.GetDirectoryName(file.Path); - string[] splitPath = directoryName.Split(Path.DirectorySeparatorChar); - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - modDirectory = splitPath[1]; - } - } - } - - if (!string.IsNullOrEmpty(modDirectory)) - { - SaveUtil.CopyFolder(Path.Combine("Mods", modDirectory), Path.Combine(SteamManager.WorkshopItemStagingFolder, "Mods", modDirectory), copySubDirs: true); - }*/ - - } - - private bool CreateWorkshopItem(Steamworks.Ugc.Item? item) - { - if (!(item?.IsInstalled ?? false)) - { - new GUIMessageBox(TextManager.Get("Error"), - TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEdit", "[itemname]", (item?.Title ?? "[NULL]"))); - return false; - } - if (!SteamManager.CreateWorkshopItemStaging(item, out itemEditor, out itemContentPackage)) - { - return false; - } - var tickBox = publishedItemList.Content.GetChildByUserData(item)?.GetAnyChild(); - if (tickBox != null) { tickBox.Selected = true; } - return true; - } - - private void ShowCreateItemFrame() - { - createItemFrame.ClearChildren(); - - if (itemEditor == null) { return; } - - if (itemContentPackage == null) - { - string errorMsg = "Failed to edit workshop item (content package null)\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.ShowCreateItemFrame:ContentPackageNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return; - } - - var createItemContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), createItemFrame.RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var topPanel = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), createItemContent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - var topLeftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), topPanel.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - var topRightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), topPanel.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - // top right column -------------------------------------------------------------------------------------- - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemTitle"), font: GUI.SubHeadingFont); - var titleBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.15f), topRightColumn.RectTransform), itemEditor?.Title); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemDescription"), font: GUI.SubHeadingFont); - - var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), topRightColumn.RectTransform)); - var descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform), itemEditor?.Description, - textAlignment: Alignment.TopLeft, style: "GUITextBoxNoBorder", font: GUI.SmallFont, wrap: true); - descriptionBox.OnTextChanged += (textBox, text) => - { - Vector2 textSize = textBox.Font.MeasureString(descriptionBox.WrappedText); - textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(descriptionContainer.Content.Rect.Height, (int)textSize.Y + 10)); - descriptionContainer.UpdateScrollBarSize(); - descriptionContainer.BarScroll = 1.0f; - itemEditor = itemEditor?.WithDescription(text); - return true; - }; - descriptionContainer.RectTransform.SizeChanged += () => { descriptionBox.Text = descriptionBox.Text; }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUI.SubHeadingFont); - var tagHolder = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.17f), topRightColumn.RectTransform) { MinSize = new Point(0, 50) }, isHorizontal: true) - { - Spacing = 5 - }; - - HashSet availableTags = new HashSet(); - foreach (string tag in itemEditor?.Tags ?? Enumerable.Empty()) - { - if (!string.IsNullOrEmpty(tag)) { availableTags.Add(tag.ToLowerInvariant()); } - } - foreach (string tag in SteamManager.PopularTags) - { - if (!string.IsNullOrEmpty(tag)) { availableTags.Add(tag.ToLowerInvariant()); } - if (availableTags.Count > 10) { break; } - } - - foreach (string tag in availableTags) - { - var tagBtn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tagHolder.Content.RectTransform, anchor: Anchor.CenterLeft), - tag.CapitaliseFirstInvariant(), style: "GUIButtonRound"); - tagBtn.TextBlock.AutoScaleHorizontal = true; - tagBtn.Selected = itemEditor?.Tags?.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)) ?? false; - - tagBtn.OnClicked = (btn, userdata) => - { - if (!tagBtn.Selected) - { - if (!(itemEditor?.Tags?.Any(t => t.ToLowerInvariant() == tag) ?? false)) { itemEditor = itemEditor?.WithTag(tagBtn.Text); } - tagBtn.Selected = true; - } - else - { - itemEditor?.Tags?.RemoveAll(t => t.Equals(tagBtn.Text, StringComparison.OrdinalIgnoreCase)); - tagBtn.Selected = false; - } - return true; - }; - } - tagHolder.UpdateScrollBarSize(); - - // top left column -------------------------------------------------------------------------------------- - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemPreviewImage"), font: GUI.SubHeadingFont); - - previewIcon = new GUIImage(new RectTransform(new Vector2(1.0f, 0.7f), topLeftColumn.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); - new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemBrowse"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - FileSelection.OnFileSelected = (file) => - { - OnPreviewImageSelected(previewIcon, file); - }; - FileSelection.ClearFileTypeFilters(); - FileSelection.AddFileTypeFilter("PNG", "*.png"); - FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); - FileSelection.AddFileTypeFilter("All files", "*.*"); - FileSelection.SelectFileTypeFilter("*.png"); - FileSelection.Open = true; - return true; - } - }; - - //if preview image has not been set, but there's a PreviewImage file inside the mod folder, use that by default - if (string.IsNullOrEmpty(itemEditor?.PreviewFile)) - { - string previewImagePath = Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName); - if (File.Exists(previewImagePath)) - { - itemEditor = itemEditor?.WithPreviewFile(Path.GetFullPath(previewImagePath)); - } - } - if (!string.IsNullOrEmpty(itemEditor?.PreviewFile)) - { - itemEditor = itemEditor?.WithPreviewFile(Path.GetFullPath(itemEditor?.PreviewFile)); - if (itemPreviewSprites.ContainsKey(itemEditor?.PreviewFile)) - { - itemPreviewSprites[itemEditor?.PreviewFile].Remove(); - } - var newPreviewImage = new Sprite(itemEditor?.PreviewFile, sourceRectangle: null); - previewIcon.Sprite = newPreviewImage; - itemPreviewSprites[itemEditor?.PreviewFile] = newPreviewImage; - } - - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemCorePackage")) - { - ToolTip = TextManager.Get("WorkshopItemCorePackageTooltip"), - Selected = itemContentPackage.IsCorePackage, - OnSelected = (tickbox) => - { - if (tickbox.Selected) - { - if (!itemContentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("ContentPackageCantMakeCorePackage", new string[2] { "[packagename]", "[missingfiletypes]" }, - new string[2] { itemContentPackage.Name, string.Join(", ", missingContentTypes) }, new bool[2] { false, true })); - tickbox.Selected = false; - } - else - { - itemContentPackage.IsCorePackage = tickbox.Selected; - } - } - else - { - itemContentPackage.IsCorePackage = false; - } - return true; - } - }; - - // file list -------------------------------------------------------------------------------------- - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), createItemContent.RectTransform), style: null); - - var fileListTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), TextManager.Get("WorkshopItemFiles"), font: GUI.SubHeadingFont); - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), fileListTitle.RectTransform, Anchor.CenterRight), TextManager.Get("WorkshopItemShowFolder"), style: "GUIButtonSmall") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => { ToolBox.OpenFileWithShell(Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path))); return true; } - }; - createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); - createItemWatcher?.Dispose(); - createItemWatcher = new System.IO.FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) - { - Filter = "*", - NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName - }; - createItemWatcher.Created += OnFileSystemChanges; - createItemWatcher.Deleted += OnFileSystemChanges; - createItemWatcher.Renamed += OnFileSystemChanges; - createItemWatcher.EnableRaisingEvents = true; - RefreshCreateItemFileList(); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.02f - }; - - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform, Anchor.TopRight), TextManager.Get("WorkshopItemRefreshFileList"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("WorkshopItemRefreshFileListTooltip"), - OnClicked = (btn, userdata) => - { - itemContentPackage = new ContentPackage(itemContentPackage.Path); - RefreshCreateItemFileList(); - return true; - } - }; - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform, Anchor.TopRight), TextManager.Get("WorkshopItemAddFiles"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - FileSelection.OnFileSelected = (file) => - { - OnAddFilesSelected(new string[] { file }); - }; - FileSelection.ClearFileTypeFilters(); - FileSelection.AddFileTypeFilter("PNG", "*.png"); - FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); - FileSelection.AddFileTypeFilter("OGG", "*.ogg"); - FileSelection.AddFileTypeFilter("XML", "*.xml"); - FileSelection.AddFileTypeFilter("TXT", "*.txt"); - FileSelection.AddFileTypeFilter("All files", "*.*"); - FileSelection.SelectFileTypeFilter("*.*"); - FileSelection.Open = true; - - return true; - } - }; - - //the item has been already published if it has a non-zero ID -> allow adding a changenote - if ((itemEditor?.FileId ?? 0) > 0) - { - var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), createItemContent.RectTransform), isHorizontal: true); - var changeNoteLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), bottomRow.RectTransform)); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), changeNoteLayout.RectTransform), TextManager.Get("WorkshopItemChangenote"), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("WorkshopItemChangenoteTooltip") - }; - - var changenoteContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), changeNoteLayout.RectTransform)); - var changenoteBox = new GUITextBox(new RectTransform(Vector2.One, changenoteContainer.Content.RectTransform), "", - textAlignment: Alignment.TopLeft, style: "GUITextBoxNoBorder", wrap: true) - { - ToolTip = TextManager.Get("WorkshopItemChangenoteTooltip") - }; - changenoteBox.OnTextChanged += (textBox, text) => - { - Vector2 textSize = textBox.Font.MeasureString(changenoteBox.WrappedText); - textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(changenoteContainer.Content.Rect.Height, (int)textSize.Y + 10)); - changenoteContainer.UpdateScrollBarSize(); - changenoteContainer.BarScroll = 1.0f; - itemEditor = itemEditor?.WithChangeLog(text); - return true; - }; - } - - var bottomButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), createItemContent.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - RelativeSpacing = 0.03f - }; - - var visibilityLabel = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), bottomButtonContainer.RectTransform), TextManager.Get("WorkshopItemVisibility"), - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("WorkshopItemVisibilityTooltip") - }; - visibilityLabel.RectTransform.MaxSize = new Point((int)(visibilityLabel.TextSize.X * 1.1f), 0); - - var visibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.2f, 1.0f), bottomButtonContainer.RectTransform)); - foreach (VisibilityType visibilityType in Enum.GetValues(typeof(VisibilityType))) - { - visibilityDropDown.AddItem(TextManager.Get("WorkshopItemVisibility." + visibilityType), visibilityType); - } - visibilityDropDown.SelectItem(itemEditor.Value.IsPublic ? VisibilityType.Public : - itemEditor.Value.IsFriendsOnly ? VisibilityType.FriendsOnly : - VisibilityType.Private); - visibilityDropDown.OnSelected = (c, ud) => - { - if (!(ud is VisibilityType visibilityType)) { return false; } - switch (visibilityType) - { - case VisibilityType.Public: - itemEditor = itemEditor?.WithPublicVisibility(); - break; - case VisibilityType.FriendsOnly: - itemEditor = itemEditor?.WithFriendsOnlyVisibility(); - break; - case VisibilityType.Private: - itemEditor = itemEditor?.WithPrivateVisibility(); - break; - } - - return true; - }; - - if ((itemEditor?.FileId ?? 0) > 0) - { - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), bottomButtonContainer.RectTransform), - TextManager.Get("WorkshopItemDelete"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("WorkshopItemDeleteTooltip"), - TextColor = GUI.Style.Red, - OnClicked = (btn, userData) => - { - if (itemEditor == null) { return false; } - var deleteVerification = new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemDeleteVerification", "[itemname]", itemEditor?.Title), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - deleteVerification.Buttons[0].OnClicked = (yesBtn, userdata) => - { - if (itemEditor == null) { return false; } - RemoveItemFromLists(itemEditor.Value.FileId); - TaskPool.Add("DeleteFileAsync", Steamworks.SteamUGC.DeleteFileAsync(itemEditor.Value.FileId), - (t) => - { - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to delete Workshop item " + (itemEditor?.Title ?? "[NULL]")); - return; - } - }); - itemEditor = null; - SelectTab(Tab.Browse); - deleteVerification.Close(); - createItemFrame.ClearChildren(); - itemContentPackage.SteamWorkshopId = 0; - itemContentPackage.Save(itemContentPackage.Path); - return true; - }; - deleteVerification.Buttons[1].OnClicked = deleteVerification.Close; - return true; - } - }; - } - var publishBtn = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), bottomButtonContainer.RectTransform, Anchor.CenterRight), - TextManager.Get((itemEditor?.FileId ?? 0) > 0 ? "WorkshopItemUpdate" : "WorkshopItemPublish")) - { - IgnoreLayoutGroups = true, - ToolTip = TextManager.Get("WorkshopItemPublishTooltip"), - OnClicked = (btn, userData) => - { - itemEditor = itemEditor?.WithTitle(titleBox.Text); - itemEditor = itemEditor?.WithDescription(descriptionBox.Text); - if (string.IsNullOrWhiteSpace(itemEditor?.Title)) - { - titleBox.Flash(GUI.Style.Red); - return false; - } - if (string.IsNullOrWhiteSpace(itemEditor?.Description)) - { - descriptionBox.Flash(GUI.Style.Red); - return false; - } - if (createItemFileList.Content.CountChildren == 0) - { - createItemFileList.Flash(GUI.Style.Red); - } - - if (!itemContentPackage.CheckErrors(out List errorMessages)) - { - new GUIMessageBox( - TextManager.GetWithVariable("workshopitempublishfailed", "[itemname]", itemEditor?.Title), - string.Join("\n", errorMessages)); - return false; - } - - PublishWorkshopItem(); - return true; - } - }; - publishBtn.TextBlock.AutoScaleHorizontal = true; - } - - private void OnPreviewImageSelected(GUIImage previewImageElement, string filePath) - { - string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); - if (new FileInfo(filePath).Length > 1024 * 1024) - { - new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); - return; - } - - if (Path.GetFullPath(filePath) != previewImagePath) - { - try - { - File.Copy(filePath, previewImagePath, overwrite: true); - } - catch (System.IO.IOException e) - { - DebugConsole.ThrowError("Failed to copy the preview image \"{previewImagePath}\" to the mod folder.", e); - return; - } - } - - if (itemPreviewSprites.ContainsKey(previewImagePath)) - { - itemPreviewSprites[previewImagePath].Remove(); - } - var newPreviewImage = new Sprite(previewImagePath, sourceRectangle: null); - previewImageElement.Sprite = newPreviewImage; - itemPreviewSprites[previewImagePath] = newPreviewImage; - itemEditor?.WithPreviewFile(previewImagePath); - } - - private void OnAddFilesSelected(string[] fileNames) - { - if (fileNames == null) { return; } - for (int i = 0; i < fileNames.Length; i++) - { - string file = fileNames[i]?.Trim(); - if (string.IsNullOrEmpty(file) || !File.Exists(file)) { continue; } - - string modFolder = Path.GetDirectoryName(itemContentPackage.Path); - string filePathRelativeToModFolder = Path.GetRelativePath(Path.Combine(Environment.CurrentDirectory, modFolder), file); - - //file is not inside the mod folder, we need to move it - if (filePathRelativeToModFolder.StartsWith("..") || - Path.GetPathRoot(Environment.CurrentDirectory) != Path.GetPathRoot(file)) - { - string destinationPath = Path.Combine(modFolder, Path.GetFileName(file)); - //add a number to the filename if a file with the same name already exists - i = 2; - while (File.Exists(destinationPath)) - { - destinationPath = Path.Combine(modFolder, $"{Path.GetFileNameWithoutExtension(file)} ({i}){Path.GetExtension(file)}"); - i++; - } - try - { - File.Copy(file, destinationPath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Copying the file \"" + file + "\" to the mod folder failed.", e); - return; - } - } - } - RefreshCreateItemFileList(); - } - - volatile bool refreshFileList = false; - - private void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) - { - refreshFileList = true; - } - - private void RefreshCreateItemFileList() - { - createItemFileList.ClearChildren(); - if (itemContentPackage == null) return; - var contentTypes = Enum.GetValues(typeof(ContentType)); - - List files = itemContentPackage.FilesUnsaved.ToList(); - - for (int i = files.Count - 1; i >= 0; i--) - { - ContentFile contentFile = files[i]; - - bool fileExists = File.Exists(contentFile.Path); - - if (contentFile.Type == ContentType.ServerExecutable) - { - fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); - } - - if (!fileExists) - { - itemContentPackage.RemoveFile(contentFile); - files.RemoveAt(i); - } - } - - List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", System.IO.SearchOption.AllDirectories) - .Select(f => new ContentFile(f, ContentType.None)) - .Where(file => Path.GetFileName(file.Path) != SteamManager.MetadataFileName && - Path.GetFileName(file.Path) != SteamManager.PreviewImageName) - .ToList(); - for (int i=0;i string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), - Path.GetFullPath(file.Path).CleanUpPath(), - StringComparison.InvariantCultureIgnoreCase)); - if (otherFile != null) - { - //replace the generated ContentFile object with the one that's present in the - //content package to determine which tickboxes should already be checked - allFiles[i] = otherFile; - files.Remove(otherFile); - } - } - - allFiles.AddRange(files); - - foreach (ContentFile contentFile in allFiles) - { - bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); - bool fileExists = File.Exists(contentFile.Path); - - if (contentFile.Type == ContentType.ServerExecutable) - { - fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); - } - - var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, - style: "ListBoxElement") - { - CanBeFocused = false, - UserData = contentFile - }; - - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), fileFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - var tickBox = new GUITickBox(new RectTransform(Vector2.One, content.RectTransform, scaleBasis: ScaleBasis.BothHeight), "") - { - Selected = itemContentPackage.FilesUnsaved.Contains(contentFile), - UserData = contentFile - }; - - tickBox.OnSelected = (tb) => - { - ContentFile f = tb.UserData as ContentFile; - if (tb.Selected) - { - if (!itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.AddFile(f); } - } - else - { - if (itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.RemoveFile(f); } - } - - return true; - }; - - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), content.RectTransform, Anchor.CenterLeft), contentFile.Path, font: GUI.SmallFont) - { - ToolTip = contentFile.Path - }; - if (!fileExists) - { - nameText.TextColor = GUI.Style.Red; - tickBox.ToolTip = TextManager.Get("WorkshopItemFileNotFound"); - } - else if (illegalPath && !ContentPackage.AllPackages.Any(cp => cp.FilesUnsaved.Any(f => Path.GetFullPath(f.Path) == Path.GetFullPath(contentFile.Path)))) - { - nameText.TextColor = GUI.Style.Red; - tickBox.ToolTip = TextManager.Get("WorkshopItemIllegalPath"); - } - - var contentTypeSelection = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), content.RectTransform, Anchor.CenterRight), - elementCount: contentTypes.Length) - { - UserData = contentFile, - }; - foreach (ContentType contentType in contentTypes) - { - contentTypeSelection.AddItem(contentType.ToString(), contentType); - } - contentTypeSelection.SelectItem(contentFile.Type); - - contentTypeSelection.OnSelected = (GUIComponent selected, object userdata) => - { - ((ContentFile)contentTypeSelection.UserData).Type = (ContentType)userdata; - itemContentPackage.Save(itemContentPackage.Path); - return true; - }; - - if (!files.Contains(contentFile)) //this prevents deletion of files not contained in the mod's path (i.e. vanilla content) - { - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), TextManager.Get("Delete"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), - TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (applyButton, obj) => - { - try - { - File.Delete(contentFile.Path); - if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); - } - //RefreshCreateItemFileList(); - RefreshMyItemList(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return true; - } - }; - } - - content.Recalculate(); - fileFrame.RectTransform.MinSize = - new Point(0, (int)(content.RectTransform.Children.Max(c => c.MinSize.Y) / content.RectTransform.RelativeSize.Y)); - nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, maxWidth: nameText.Rect.Width); - } - - itemContentPackage.Save(itemContentPackage.Path); - } - - private void PublishWorkshopItem() - { - if (itemContentPackage == null || itemEditor == null) { return; } - -#if UNSTABLE - var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("unstableworkshopitempublishwarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - var workshopPublishStatus = SteamManager.StartPublishItem(itemContentPackage, itemEditor); - if (workshopPublishStatus != null) - { - if (!(itemEditor?.HasTag("unstable") ?? false)) { itemEditor = itemEditor?.WithTag("unstable"); } - CoroutineManager.StartCoroutine(WaitForPublish(workshopPublishStatus), "WaitForPublish"); - } - msgBox.Close(); - return true; - }; - msgBox.Buttons[1].OnClicked += msgBox.Close; -#else - itemEditor = itemEditor?.WithoutTag("unstable"); - var workshopPublishStatus = SteamManager.StartPublishItem(itemContentPackage, itemEditor); - if (workshopPublishStatus == null) { return; } - CoroutineManager.StartCoroutine(WaitForPublish(workshopPublishStatus), "WaitForPublish"); -#endif - - } - - private IEnumerable WaitForPublish(SteamManager.WorkshopPublishStatus workshopPublishStatus) - { - var item = workshopPublishStatus.Item; - var coroutine = workshopPublishStatus.Coroutine; - - string pleaseWaitText = TextManager.Get("WorkshopPublishPleaseWait"); - var msgBox = new GUIMessageBox( - pleaseWaitText, - TextManager.GetWithVariable("WorkshopPublishInProgress", "[itemname]", item?.Title), - new string[] { TextManager.Get("Cancel") }); - - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - CoroutineManager.StopCoroutines("WaitForPublish"); - createItemFrame.ClearChildren(); - SelectTab(Tab.Browse); - msgBox.Close(); - return true; - }; - - yield return CoroutineStatus.Running; - while (CoroutineManager.IsCoroutineRunning(coroutine)) - { - msgBox.Header.Text = pleaseWaitText + new string('.', ((int)Timing.TotalTime % 3 + 1)); - yield return CoroutineStatus.Running; - } - msgBox.Close(); - - if (workshopPublishStatus.Success ?? false) - { - new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemPublished", "[itemname]", item?.Title)); - } - else - { - string errorMsg = workshopPublishStatus.Result.HasValue ? - TextManager.GetWithVariable("WorkshopPublishError." + workshopPublishStatus.Result?.Result.ToString(), "[savepath]", SaveUtil.SaveFolder, returnNull: true) : - null; - - if (errorMsg == null) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariable("WorkshopItemPublishFailed", "[itemname]", item?.Title) + - (workshopPublishStatus?.TaskStatus != null ? - " Task ended with status " +workshopPublishStatus?.TaskStatus?.ToString() : - " Publish failed with result "+ workshopPublishStatus.Result?.Result.ToString())); - } - else - { - new GUIMessageBox(TextManager.Get("Error"), errorMsg); - } - } - - createItemFrame.ClearChildren(); - SelectTab(Tab.Browse); - } - -#region UI management - - public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) - { - graphics.Clear(Color.CornflowerBlue); - - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); - - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); - } - - public override void AddToGUIUpdateList() - { - menu.AddToGUIUpdateList(); - } - - public override void Update(double deltaTime) - { - if (refreshFileList) - { - RefreshCreateItemFileList(); - refreshFileList = false; - } - } - -#endregion - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 6b6b947dc..09ab544ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Xml.Linq; @@ -16,12 +17,16 @@ using System.IO; using Barotrauma.IO; #endif -// ReSharper disable AccessToModifiedClosure, PossibleLossOfFraction, RedundantLambdaParameterType, UnusedVariable - namespace Barotrauma { class SubEditorScreen : EditorScreen { + private static Submarine MainSub + { + get => Submarine.MainSub; + set => Submarine.MainSub = value; + } + private enum LayerVisibility { Visible, @@ -53,13 +58,15 @@ namespace Barotrauma islinked = Linkage; } } - - private static readonly string[] crewExperienceLevels = + + #warning TODO: switch this to an enum? + private static readonly ImmutableArray crewExperienceLevels = new string[] { "CrewExperienceLow", "CrewExperienceMid", "CrewExperienceHigh" - }; + }.ToImmutableArray(); + public enum Mode { @@ -144,7 +151,7 @@ namespace Barotrauma private GUIDropDown linkedSubBox; private static GUIComponent autoSaveLabel; - private readonly static int maxAutoSaves = GameSettings.MaximumAutoSaves; + private static int maxAutoSaves => GameSettings.CurrentConfig.MaxAutoSaves; public static readonly object ItemAddMutex = new object(), ItemRemoveMutex = new object(); @@ -231,22 +238,26 @@ namespace Barotrauma private static string GetSubDescription() { - string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Info.Name ?? ""), true); - if (localizedDescription != null) { return localizedDescription; } - return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Info.Description; + if (MainSub?.Info != null) + { + LocalizedString localizedDescription = TextManager.Get($"submarine.description.{MainSub.Info.Name ?? ""}"); + if (!localizedDescription.IsNullOrEmpty()) { return localizedDescription.Value; } + return MainSub.Info.Description?.Value ?? ""; + } + return ""; } - private static string GetTotalHullVolume() + private static LocalizedString GetTotalHullVolume() { - return TextManager.Get("TotalHullVolume") + ":\n" + Hull.hullList.Sum(h => h.Volume); + return $"{TextManager.Get("TotalHullVolume")}:\n{Hull.HullList.Sum(h => h.Volume)}"; } - private static string GetSelectedHullVolume() + private static LocalizedString GetSelectedHullVolume() { float buoyancyVol = 0.0f; float selectedVol = 0.0f; float neutralPercentage = SubmarineBody.NeutralBallastPercentage; - Hull.hullList.ForEach(h => + Hull.HullList.ForEach(h => { buoyancyVol += h.Volume; if (h.IsSelected) @@ -255,16 +266,16 @@ namespace Barotrauma } }); buoyancyVol *= neutralPercentage; - string retVal = TextManager.Get("SelectedHullVolume") + ":\n" + selectedVol; + string retVal = $"{TextManager.Get("SelectedHullVolume")}:\n{selectedVol}"; if (selectedVol > 0.0f && buoyancyVol > 0.0f) { if (buoyancyVol / selectedVol < 1.0f) { - retVal += " (" + TextManager.GetWithVariable("OptimalBallastLevel", "[value]", (buoyancyVol / selectedVol).ToString("0.0000")) + ")"; + retVal += $" ({TextManager.GetWithVariable("OptimalBallastLevel", "[value]", (buoyancyVol / selectedVol).ToString("0.0000"))})"; } else { - retVal += " (" + TextManager.Get("InsufficientBallast") + ")"; + retVal += $" ({TextManager.Get("InsufficientBallast")})"; } } return retVal; @@ -340,7 +351,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SaveButton") { - ToolTip = TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖"), OnClicked = (btn, data) => { loadFrame = null; @@ -419,7 +430,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); subNameLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.9f), paddedTopPanel.RectTransform, Anchor.CenterLeft), - TextManager.Get("unspecifiedsubfilename"), font: GUI.LargeFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("unspecifiedsubfilename"), font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft); linkedSubBox = new GUIDropDown(new RectTransform(new Vector2(0.15f, 0.9f), paddedTopPanel.RectTransform), TextManager.Get("AddSubButton"), elementCount: 20) @@ -452,7 +463,7 @@ namespace Barotrauma defaultModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditSubButton") { - ToolTip = TextManager.Get("SubEditorEditingMode") + "‖color:125,125,125‖\nCtrl + 1‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("SubEditorEditingMode") + "‖color:125,125,125‖\nCtrl + 1‖color:end‖"), OnSelected = tBox => { if (!lockMode) @@ -468,7 +479,7 @@ namespace Barotrauma wiringModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "WiringModeButton") { - ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip") + "‖color:125,125,125‖\nCtrl + 2‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip") + "‖color:125,125,125‖\nCtrl + 2‖color:end‖"), OnSelected = tBox => { if (!lockMode) @@ -496,7 +507,7 @@ namespace Barotrauma { if (GenerateWaypoints()) { - GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green); } WayPoint.ShowWayPoints = true; generateWaypointsVerification.Close(); @@ -508,7 +519,7 @@ namespace Barotrauma { if (GenerateWaypoints()) { - GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green); } WayPoint.ShowWayPoints = true; @@ -654,9 +665,9 @@ namespace Barotrauma Color = Color.Black, Visible = false }; - new GUITextBlock(new RectTransform(Vector2.One, undoBufferDisclaimer.RectTransform, Anchor.Center), text: TextManager.Get("editor.undounavailable"), textAlignment: Alignment.Center, wrap: true, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(Vector2.One, undoBufferDisclaimer.RectTransform, Anchor.Center), text: TextManager.Get("editor.undounavailable"), textAlignment: Alignment.Center, wrap: true, font: GUIStyle.SubHeadingFont) { - TextColor = GUI.Style.Orange + TextColor = GUIStyle.Orange }; UpdateUndoHistoryPanel(); @@ -742,7 +753,7 @@ namespace Barotrauma }; showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox)); - var subcategoryHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("subcategories"), font: GUI.SubHeadingFont); + var subcategoryHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("subcategories"), font: GUIStyle.SubHeadingFont); subcategoryHeader.RectTransform.MinSize = new Point(0, (int)(subcategoryHeader.Rect.Height * 1.5f)); var subcategoryList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform) { MinSize = new Point(0, showEntitiesPanel.Rect.Height / 3) }); @@ -757,7 +768,7 @@ namespace Barotrauma foreach (string subcategory in availableSubcategories) { var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), - TextManager.Get("subcategory." + subcategory, returnNull: true) ?? subcategory, font: GUI.SmallFont) + TextManager.Get("subcategory." + subcategory).Fallback(subcategory), font: GUIStyle.SmallFont) { UserData = subcategory, Selected = !IsSubcategoryHidden(subcategory), @@ -766,7 +777,7 @@ namespace Barotrauma if (tb.TextBlock.TextSize.X > tb.TextBlock.Rect.Width * 1.25f) { tb.ToolTip = tb.Text; - tb.Text = ToolBox.LimitString(tb.Text, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f)); + tb.Text = ToolBox.LimitString(tb.Text.Value, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f)); } } @@ -780,7 +791,7 @@ namespace Barotrauma //----------------------------------------------- - float longestTextWidth = GUI.SmallFont.MeasureString(TextManager.Get("SubEditorShadowCastingLights")).X; + float longestTextWidth = GUIStyle.SmallFont.MeasureString(TextManager.Get("SubEditorShadowCastingLights")).X; entityCountPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas) { MinSize = new Point(Math.Max(170, (int)(longestTextWidth * 1.5f)), 0), @@ -794,35 +805,35 @@ namespace Barotrauma }; var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Items"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); itemCount.TextGetter = () => { - itemCount.TextColor = ToolBox.GradientLerp(Item.ItemList.Count / 5000.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + itemCount.TextColor = ToolBox.GradientLerp(Item.ItemList.Count / 5000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return Item.ItemList.Count.ToString(); }; var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => { - int count = (MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.hullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count); - structureCount.TextColor = ToolBox.GradientLerp(count / 1000.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + int count = (MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count); + structureCount.TextColor = ToolBox.GradientLerp(count / 1000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return count.ToString(); }; var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Walls"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); wallCount.TextGetter = () => { - wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return Structure.WallList.Count.ToString(); }; var lightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorLights"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); lightCountText.TextGetter = () => { @@ -832,11 +843,11 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(); } - lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return lightCount.ToString(); }; var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, wrap: true); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, wrap: true); var shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); shadowCastingLightCountText.TextGetter = () => { @@ -846,7 +857,7 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(l => l.CastShadows); } - shadowCastingLightCountText.TextColor = ToolBox.GradientLerp(lightCount / 60.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + shadowCastingLightCountText.TextColor = ToolBox.GradientLerp(lightCount / 60.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return lightCount.ToString(); }; entityCountPanel.RectTransform.NonScaledSize = @@ -861,11 +872,11 @@ namespace Barotrauma { Visible = false }; - GUITextBlock totalHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform), "", font: GUI.SmallFont) + GUITextBlock totalHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform), "", font: GUIStyle.SmallFont) { TextGetter = GetTotalHullVolume }; - GUITextBlock selectedHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.5f) }, "", font: GUI.SmallFont) + GUITextBlock selectedHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.5f) }, "", font: GUIStyle.SmallFont) { TextGetter = GetSelectedHullVolume }; @@ -889,7 +900,7 @@ namespace Barotrauma { Visible = false }; - var saveStampButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), snapToGridFrame.RectTransform, Anchor.Center), TextManager.Get("subeditor.snaptogrid", fallBackTag: "spriteeditor.snaptogrid")); + var saveStampButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), snapToGridFrame.RectTransform, Anchor.Center), TextManager.Get("subeditor.snaptogrid", "spriteeditor.snaptogrid")); saveStampButton.TextBlock.AutoScaleHorizontal = true; saveStampButton.OnClicked += (btn, userdata) => { @@ -906,7 +917,7 @@ namespace Barotrauma toggleEntityMenuButton = new GUIButton(new RectTransform(new Vector2(0.15f, 0.08f), EntityMenu.RectTransform, Anchor.TopCenter, Pivot.BottomCenter) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical") { - ToolTip = TextManager.Get("EntityMenuToggleTooltip") + "‖color:125,125,125‖\nQ‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("EntityMenuToggleTooltip") + "‖color:125,125,125‖\nQ‖color:end‖"), OnClicked = (btn, userdata) => { entityMenuOpen = !entityMenuOpen; @@ -934,11 +945,11 @@ namespace Barotrauma { CanBeFocused = false }; - selectedCategoryText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("MapEntityCategory.All"), font: GUI.LargeFont); + selectedCategoryText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("MapEntityCategory.All"), font: GUIStyle.LargeFont); - var filterText = new GUITextBlock(new RectTransform(new Vector2(0.1f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.SubHeadingFont); + var filterText = new GUITextBlock(new RectTransform(new Vector2(0.1f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont); filterText.RectTransform.MaxSize = new Point((int)(filterText.TextSize.X * 1.5f), int.MaxValue); - entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.17f, 1.0f), entityMenuTop.RectTransform), font: GUI.Font, createClearButton: true); + entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.17f, 1.0f), entityMenuTop.RectTransform), font: GUIStyle.Font, createClearButton: true); entityFilterBox.OnTextChanged += (textBox, text) => { if (text == lastFilter) { return true; } @@ -997,9 +1008,9 @@ namespace Barotrauma private bool TestSubmarine(GUIButton button, object obj) { - List errorMsgs = new List(); + List errorMsgs = new List(); - if (!Hull.hullList.Any()) + if (!Hull.HullList.Any()) { errorMsgs.Add(TextManager.Get("NoHullsWarning")); } @@ -1011,13 +1022,13 @@ namespace Barotrauma if (errorMsgs.Any()) { - new GUIMessageBox(TextManager.Get("Error"), string.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); + new GUIMessageBox(TextManager.Get("Error"), LocalizedString.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); return true; } CloseItem(); - backedUpSubInfo = new SubmarineInfo(Submarine.MainSub); + backedUpSubInfo = new SubmarineInfo(MainSub); GameMain.GameScreen.Select(); @@ -1042,14 +1053,14 @@ namespace Barotrauma categorizedEntityList.Content.ClearChildren(); allEntityList.Content.ClearChildren(); - int maxTextWidth = (int)(GUI.SubHeadingFont.MeasureString(TextManager.Get("mapentitycategory.misc")).X + GUI.IntScale(50)); + int maxTextWidth = (int)(GUIStyle.SubHeadingFont.MeasureString(TextManager.Get("mapentitycategory.misc")).X + GUI.IntScale(50)); Dictionary> entityLists = new Dictionary>(); Dictionary categoryKeys = new Dictionary(); foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory))) { - string categoryName = TextManager.Get("MapEntityCategory." + category); - maxTextWidth = (int)Math.Max(maxTextWidth, GUI.SubHeadingFont.MeasureString(categoryName.Replace(' ', '\n')).X + GUI.IntScale(50)); + LocalizedString categoryName = TextManager.Get("MapEntityCategory." + category); + maxTextWidth = (int)Math.Max(maxTextWidth, GUIStyle.SubHeadingFont.MeasureString(categoryName.Replace(" ", "\n")).X + GUI.IntScale(50)); foreach (MapEntityPrefab ep in MapEntityPrefab.List) { if (!ep.Category.HasFlag(category)) { continue; } @@ -1060,10 +1071,10 @@ namespace Barotrauma } entityLists[category + ep.Subcategory].Add(ep); categoryKeys[category + ep.Subcategory] = category; - string subcategoryName = TextManager.Get("subcategory." + ep.Subcategory, returnNull: true) ?? ep.Subcategory; + LocalizedString subcategoryName = TextManager.Get("subcategory." + ep.Subcategory).Fallback(ep.Subcategory); if (subcategoryName != null) { - maxTextWidth = (int)Math.Max(maxTextWidth, GUI.SubHeadingFont.MeasureString(subcategoryName.Replace(' ', '\n')).X + GUI.IntScale(50)); + maxTextWidth = (int)Math.Max(maxTextWidth, GUIStyle.SubHeadingFont.MeasureString(subcategoryName.Replace(" ", "\n")).X + GUI.IntScale(50)); } } } @@ -1080,12 +1091,12 @@ namespace Barotrauma new GUIFrame(new RectTransform(Vector2.One, categoryFrame.RectTransform), style: "HorizontalLine"); - string categoryName = TextManager.Get("MapEntityCategory." + entityLists[categoryKey].First().Category); - string subCategoryName = entityLists[categoryKey].First().Subcategory; - if (string.IsNullOrEmpty(subCategoryName)) + LocalizedString categoryName = TextManager.Get("MapEntityCategory." + entityLists[categoryKey].First().Category); + LocalizedString subCategoryName = entityLists[categoryKey].First().Subcategory; + if (subCategoryName.IsNullOrEmpty()) { new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft), - categoryName, textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont, wrap: true) + categoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont, wrap: true) { Padding = new Vector4(GUI.IntScale(10)) }; @@ -1093,16 +1104,16 @@ namespace Barotrauma } else { - subCategoryName = string.IsNullOrEmpty(subCategoryName) ? + subCategoryName = subCategoryName.IsNullOrEmpty() ? TextManager.Get("mapentitycategory.misc") : - (TextManager.Get("subcategory." + subCategoryName, returnNull: true) ?? subCategoryName); + (TextManager.Get($"subcategory.{subCategoryName}").Fallback(subCategoryName)); var categoryTitle = new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft), - categoryName, textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + categoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, wrap: true) { Padding = new Vector4(GUI.IntScale(10)) }; new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(0, (int)(categoryTitle.TextSize.Y + GUI.IntScale(10))) }, - subCategoryName, textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont, wrap: true) + subCategoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont, wrap: true) { Padding = new Vector4(GUI.IntScale(10)) }; @@ -1136,9 +1147,9 @@ namespace Barotrauma categoryFrame.RectTransform.MinSize = new Point(0, contentHeight); entityListInner.RectTransform.NonScaledSize = new Point(entityListInner.Rect.Width, contentHeight); entityListInner.RectTransform.MinSize = new Point(0, contentHeight); - + entityListInner.Content.RectTransform.SortChildren((i1, i2) => - string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); + string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData)?.Name.Value, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name.Value, StringComparison.Ordinal)); } foreach (MapEntityPrefab ep in MapEntityPrefab.List) @@ -1167,14 +1178,13 @@ namespace Barotrauma frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); - string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; + LocalizedString name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; + frame.ToolTip = ep.Description.IsNullOrEmpty() ? name : name + '\n' + ep.Description; if (ep.ContentPackage != GameMain.VanillaContent && ep.ContentPackage != null) { frame.Color = Color.Magenta; - string colorStr = XMLExtensions.ColorToString(Color.MediumPurple); - frame.ToolTip += $"\n‖color:{colorStr}‖{ep.ContentPackage?.Name}‖color:end‖"; + frame.ToolTip = RichString.Rich($"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"); } if (ep.HideInMenus) { @@ -1189,7 +1199,7 @@ namespace Barotrauma CanBeFocused = false }; - Sprite icon = ep.sprite; + Sprite icon = ep.Sprite; Color iconColor = Color.White; if (ep is ItemPrefab itemPrefab) { @@ -1204,7 +1214,7 @@ namespace Barotrauma } } GUIImage img = null; - if (ep.sprite != null) + if (ep.Sprite != null) { img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon) @@ -1226,16 +1236,22 @@ namespace Barotrauma }) { HideElementsOutsideFrame = true, - ToolTip = frame.RawToolTip + ToolTip = frame.ToolTip.SanitizedString }; } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) + text: name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false }; - if (legacy) textBlock.TextColor *= 0.6f; + if (legacy) { textBlock.TextColor *= 0.6f; } + if (name.IsNullOrEmpty()) + { + DebugConsole.AddWarning($"Entity \"{ep.Identifier.Value}\" has no name!"); + textBlock.Text = frame.ToolTip = ep.Identifier.Value; + textBlock.TextColor = GUIStyle.Red; + } textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); if (ep.Category == MapEntityCategory.ItemAssembly) @@ -1338,7 +1354,7 @@ namespace Barotrauma Submarine.Unload(); } - string name = (Submarine.MainSub == null) ? TextManager.Get("unspecifiedsubfilename") : Submarine.MainSub.Info.Name; + string name = (MainSub == null) ? TextManager.Get("unspecifiedsubfilename").Value : MainSub.Info.Name; if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; } subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); @@ -1349,21 +1365,21 @@ namespace Barotrauma if (backedUpSubInfo != null) { - Submarine.MainSub = new Submarine(backedUpSubInfo); + MainSub = new Submarine(backedUpSubInfo); if (previewImage != null && backedUpSubInfo.PreviewImage?.Texture != null && !backedUpSubInfo.PreviewImage.Texture.IsDisposed) { previewImage.Sprite = backedUpSubInfo.PreviewImage; } backedUpSubInfo = null; } - else if (Submarine.MainSub == null) + else if (MainSub == null) { var subInfo = new SubmarineInfo(); - Submarine.MainSub = new Submarine(subInfo); + MainSub = new Submarine(subInfo); } - Submarine.MainSub.UpdateTransform(interpolate: false); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + MainSub.UpdateTransform(interpolate: false); + cam.Position = MainSub.Position + MainSub.HiddenSubPosition; GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f); GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); @@ -1389,7 +1405,7 @@ namespace Barotrauma CreateDummyCharacter(); - if (GameSettings.EnableSubmarineAutoSave && enableAutoSave) + if (GameSettings.CurrentConfig.EnableSubmarineAutoSave && enableAutoSave) { CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); } @@ -1397,7 +1413,7 @@ namespace Barotrauma ImageManager.OnEditorSelected(); ReconstructLayers(); - if (!GameMain.Config.EditorDisclaimerShown) + if (!GameSettings.CurrentConfig.EditorDisclaimerShown) { GameMain.Instance.ShowEditorDisclaimer(); } @@ -1416,7 +1432,7 @@ namespace Barotrauma return; } - string body = TextManager.GetWithVariable("SubEditor.LoadConfirmBody", "[submarine]", info.Name); + LocalizedString body = TextManager.GetWithVariable("SubEditor.LoadConfirmBody", "[submarine]", info.Name); GUI.AskForConfirmation(TextManager.Get("Load"), body, onConfirm: () => LoadSub(info), onDeny: () => info.Dispose()); break; @@ -1434,9 +1450,9 @@ namespace Barotrauma Texture2D texture = Sprite.LoadTexture(filePath); previewImage.Sprite = new Sprite(texture, null, null); - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; + MainSub.Info.PreviewImage = previewImage.Sprite; } break; @@ -1454,7 +1470,7 @@ namespace Barotrauma /// private static IEnumerable AutoSaveCoroutine() { - DateTime target = DateTime.Now.AddMinutes(GameSettings.AutoSaveIntervalSeconds); + DateTime target = DateTime.Now.AddMinutes(GameSettings.CurrentConfig.AutoSaveIntervalSeconds); DateTime tempTarget = DateTime.Now; bool wasPaused = false; @@ -1495,7 +1511,7 @@ namespace Barotrauma TimeSpan timeInEditor = DateTime.Now - editorSelectedTime; #if USE_STEAM - SteamAchievementManager.IncrementStat("hoursineditor", (float)timeInEditor.TotalHours); + SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); #endif GUI.ForceMouseOn(null); @@ -1510,9 +1526,9 @@ namespace Barotrauma SetMode(Mode.Default); - SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume); + SoundPlayer.OverrideMusicType = Identifier.Empty; + GameMain.SoundManager.SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume); if (CoroutineManager.IsCoroutineRunning("SubEditorAutoSave")) { @@ -1541,6 +1557,10 @@ namespace Barotrauma ClearFilter(); ClearLayers(); + while (packageReloadQueue.TryDequeue(out var p)) + { + ContentPackageManager.ReloadContentPackage(p); + } } private void CreateDummyCharacter() @@ -1572,15 +1592,15 @@ namespace Barotrauma /// The saving is ran in another thread to avoid lag spikes private static void AutoSave() { - if (MapEntity.mapEntityList.Any() && GameSettings.EnableSubmarineAutoSave && !isAutoSaving) + if (MapEntity.mapEntityList.Any() && GameSettings.CurrentConfig.EnableSubmarineAutoSave && !isAutoSaving) { - if (Submarine.MainSub != null) + if (MainSub != null) { isAutoSaving = true; if (!Directory.Exists(autoSavePath)) { return; } XDocument doc = new XDocument(new XElement("Submarine")); - Submarine.MainSub.SaveToXElement(doc.Root); + MainSub.SaveToXElement(doc.Root); Thread saveThread = new Thread(start => { try @@ -1592,13 +1612,14 @@ namespace Barotrauma CrossThread.RequestExecutionOnMainThread(() => { - if (AutoSaveInfo?.Root == null || Submarine.MainSub?.Info == null) { return; } + if (AutoSaveInfo?.Root == null || MainSub?.Info == null) { return; } int saveCount = AutoSaveInfo.Root.Elements().Count(); while (AutoSaveInfo.Root.Elements().Count() > maxAutoSaves) { XElement min = AutoSaveInfo.Root.Elements().OrderBy(element => element.GetAttributeUInt64("time", 0)).FirstOrDefault(); - string path = min.GetAttributeString("file", ""); + #warning TODO: revise + string path = min.GetAttributeStringUnrestricted("file", ""); if (string.IsNullOrWhiteSpace(path)) { continue; } if (IO.File.Exists(path)) { IO.File.Delete(path); } @@ -1607,7 +1628,7 @@ namespace Barotrauma XElement newElement = new XElement("AutoSave", new XAttribute("file", filePath), - new XAttribute("name", Submarine.MainSub.Info.Name), + new XAttribute("name", MainSub.Info.Name), new XAttribute("time", (ulong)time.TotalSeconds)); AutoSaveInfo.Root.Add(newElement); @@ -1640,7 +1661,7 @@ namespace Barotrauma if (Selected != GameMain.SubEditorScreen) { return; } autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); - string label = TextManager.Get("AutoSaved"); + LocalizedString label = TextManager.Get("AutoSaved"); autoSaveLabel = new GUILayoutGroup(new RectTransform(new Point(GUI.IntScale(150), GUI.IntScale(32)), GameMain.SubEditorScreen.EntityMenu.RectTransform, Anchor.TopRight) { ScreenSpaceOffset = new Point(-GUI.IntScale(16), -GUI.IntScale(48)) @@ -1650,7 +1671,7 @@ namespace Barotrauma }; GUIImage checkmark = new GUIImage(new RectTransform(new Vector2(0.25f, 1f), autoSaveLabel.RectTransform), style: "MissionCompletedIcon", scaleToFit: true); - GUITextBlock labelComponent = new GUITextBlock(new RectTransform(new Vector2(0.75f, 1f), autoSaveLabel.RectTransform), label, font: GUI.SubHeadingFont, color: GUI.Style.Green) + GUITextBlock labelComponent = new GUITextBlock(new RectTransform(new Vector2(0.75f, 1f), autoSaveLabel.RectTransform), label, font: GUIStyle.SubHeadingFont, color: GUIStyle.Green) { Padding = Vector4.Zero, AutoScaleHorizontal = true, @@ -1666,47 +1687,43 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(nameBox.Text)) { - GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); nameBox.Flash(); return false; } string specialSavePath = ""; - if (Submarine.MainSub.Info.Type != SubmarineType.Player) + if (MainSub.Info.Type != SubmarineType.Player) { - ContentType contentType = ContentType.Submarine; - switch (Submarine.MainSub.Info.Type) + Identifier typeIdentifier = MainSub.Info.Type.ToString().ToIdentifier(); + Type contentType = ContentFile.Types.FirstOrDefault(t + => !t.Type.IsAbstract + && t.Type.IsSubclassOf(typeof(BaseSubFile)) + && t.Names.Contains(typeIdentifier)) + ?.Type ?? + typeof(SubmarineFile); + if (MainSub.Info.Type == SubmarineType.OutpostModule && + MainSub.Info.OutpostModuleInfo != null) { - case SubmarineType.OutpostModule: - if (Submarine.MainSub.Info?.OutpostModuleInfo != null) - { - contentType = ContentType.OutpostModule; - Submarine.MainSub.Info.PreviewImage = null; - } - break; - case SubmarineType.Outpost: - contentType = ContentType.Outpost; - break; - case SubmarineType.Wreck: - contentType = ContentType.Wreck; - break; - case SubmarineType.EnemySubmarine: - contentType = ContentType.EnemySubmarine; - break; + contentType = typeof(OutpostModuleFile); + MainSub.Info.PreviewImage = null; } - if (contentType != ContentType.Submarine) + + if (contentType != typeof(SubmarineFile)) { #if DEBUG - var existingFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), contentType); - if (contentType == ContentType.OutpostModule) + var existingFiles = GameMain.VanillaContent.GetFiles(contentType); + if (contentType == typeof(OutpostModuleFile)) { - existingFiles = existingFiles.Where(f => f.Path.Contains("Ruin") == Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin")); + existingFiles = existingFiles.Where(f => f.Path.Value.Contains("Ruin") == MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier())); } #else - var existingFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages.Where(c => c != GameMain.VanillaContent), contentType); + var existingFiles = ContentPackageManager.EnabledPackages.All + .Where(c => c != GameMain.VanillaContent) + .SelectMany(c => c.GetFiles(contentType)); #endif - specialSavePath = existingFiles.FirstOrDefault(f => - Path.GetFullPath(f.Path) != Path.GetFullPath(SubmarineInfo.SavePath) && ContentPackage.IsModFilePathAllowed(f.Path))?.Path; + specialSavePath = existingFiles.FirstOrDefault(f => + ContentPackage.PathAllowedAsLocalModFile(f.Path.Value))?.Path.Value; if (!string.IsNullOrEmpty(specialSavePath)) { @@ -1714,9 +1731,9 @@ namespace Barotrauma } } } - else if (Submarine.MainSub.Info.SubmarineClass == SubmarineClass.Undefined && !Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle)) + else if (MainSub.Info.SubmarineClass == SubmarineClass.Undefined && !MainSub.Info.HasTag(SubmarineTag.Shuttle)) { - var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("undefinedsubmarineclasswarning"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("undefinedsubmarineclasswarning"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked = (bt, userdata) => { @@ -1734,16 +1751,16 @@ namespace Barotrauma } if (!string.IsNullOrEmpty(specialSavePath) && - (string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) || Path.GetFileNameWithoutExtension(Submarine.MainSub.Info.Name) != nameBox.Text || Path.GetDirectoryName(Submarine.MainSub?.Info.FilePath) != specialSavePath)) + (string.IsNullOrEmpty(MainSub?.Info.FilePath) || Path.GetFileNameWithoutExtension(MainSub.Info.Name) != nameBox.Text || Path.GetDirectoryName(MainSub?.Info.FilePath) != specialSavePath)) { - string submarineTypeTag = "SubmarineType." + Submarine.MainSub.Info.Type; - if (Submarine.MainSub.Info.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(submarineTypeTag)) + string submarineTypeTag = $"SubmarineType.{MainSub.Info.Type}"; + if (MainSub.Info.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(submarineTypeTag)) { submarineTypeTag = "MissionType.Pirate"; } var msgBox = new GUIMessageBox("", TextManager.GetWithVariables("savesubtospecialfolderprompt", - new string[] { "[type]", "[outpostpath]" }, new string[] { TextManager.Get(submarineTypeTag), specialSavePath }), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + ("[type]", TextManager.Get(submarineTypeTag)), ("[outpostpath]", specialSavePath)), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked = (bt, userdata) => { SaveSubToFile(nameBox.Text, specialSavePath); @@ -1766,45 +1783,87 @@ namespace Barotrauma return result; } + private readonly Queue packageReloadQueue = new Queue(); + + private void EnqueueForReload(ContentPackage p) + { + if (p is null) { return; } + if (!packageReloadQueue.Contains(p)) { packageReloadQueue.Enqueue(p); } + } + private bool SaveSubToFile(string name, string specialSavePath = null) { + bool canModifyPackage(ContentPackage p) + => p != null && ContentPackageManager.LocalPackages.Contains(p) && p != ContentPackageManager.VanillaCorePackage; + + Type subFileType = MainSub?.Info.Type switch + { + SubmarineType.Outpost => typeof(OutpostFile), + SubmarineType.OutpostModule => typeof(OutpostModuleFile), + SubmarineType.Ruin => typeof(OutpostModuleFile), + SubmarineType.Wreck => typeof(WreckFile), + SubmarineType.BeaconStation => typeof(BeaconStationFile), + SubmarineType.EnemySubmarine => typeof(EnemySubmarineFile), + SubmarineType.Player => typeof(SubmarineFile) + }; + + void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) + { + filePath = filePath.CleanUpPath(); + packagePath = packagePath.CleanUpPath(); + string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false); + if (filePath.StartsWith(packageDir)) + { + filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; + } + if (!modProject.Files.Any(f => f.Type == subFileType && + f.Path == filePath)) + { + var newFile = ModProject.File.FromPath(filePath, subFileType); + modProject.AddFile(newFile); + } + + modProject.DiscardHashAndInstallTime(); + modProject.Save(packagePath); + } + if (string.IsNullOrWhiteSpace(name)) { - GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); return false; } foreach (var illegalChar in Path.GetInvalidFileNameChars()) { if (!name.Contains(illegalChar)) continue; - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } + string newLocalModDir = $"{ContentPackage.LocalModsDir}/{name}"; + + var vanilla = GameMain.VanillaContent; + var vanillaSubs = vanilla?.GetFiles()?.Select(f => f.Path); + bool isVanillaSub = vanillaSubs?.Any(f => f.Value == MainSub.Info.FilePath.CleanUpPath()) ?? false; + string savePath = name + ".sub"; string prevSavePath = null; - string directoryName = Submarine.MainSub?.Info?.FilePath == null ? - SubmarineInfo.SavePath : Path.GetDirectoryName(Submarine.MainSub.Info.FilePath); if (!string.IsNullOrEmpty(specialSavePath)) { - directoryName = specialSavePath; + string directoryName = specialSavePath; savePath = Path.Combine(directoryName, savePath); - ContentPackage contentPackage = GameMain.Config.AllEnabledPackages.FirstOrDefault(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path) == directoryName)); + ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path.Value) == directoryName)); - bool allowSavingToVanilla = false; -#if DEBUG - allowSavingToVanilla = true; -#endif - if (!contentPackage.Files.Any(f => Path.GetFullPath(f.Path) == Path.GetFullPath(savePath)) && (allowSavingToVanilla || contentPackage != GameMain.VanillaContent)) + if (!contentPackage.Files.Any(f => f.Path == savePath) && canModifyPackage(contentPackage)) { var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("addtocontentpackageprompt", "[packagename]", contentPackage.Name), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked = (bt, userdata) => { - contentPackage.AddFile(savePath, ContentType.OutpostModule); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - contentPackage.Save(contentPackage.Path, reload: false); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + ModProject modProject = new ModProject(contentPackage); + addSubAndSaveModProject(modProject, savePath, contentPackage.Path); + EnqueueForReload(contentPackage); + msgBox.Close(); return true; }; @@ -1815,72 +1874,58 @@ namespace Barotrauma }; } } - else if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && - Submarine.MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + else if (!string.IsNullOrEmpty(MainSub?.Info.FilePath) && + MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { - prevSavePath = Submarine.MainSub.Info.FilePath.CleanUpPath(); - string prevDir = Path.GetDirectoryName(Submarine.MainSub.Info.FilePath).CleanUpPath(); + prevSavePath = MainSub.Info.FilePath.CleanUpPath(); + string prevDir = Path.GetDirectoryName(MainSub.Info.FilePath).CleanUpPath(); string[] subDirs = prevDir.Split('/'); - bool forceToSubFolder = Steam.SteamManager.IsInitialized; - bool isInSubFolder = subDirs.Length > 0 && subDirs[0].Equals("Submarines", StringComparison.InvariantCultureIgnoreCase); - if (forceToSubFolder && subDirs.Length > 1 && subDirs[0].Equals("Mods", StringComparison.InvariantCultureIgnoreCase)) + + ModProject modProject = new ModProject() { Name = name }; + string fileListPath = null; + + if (subDirs.Length > 1 && subDirs[0].Equals(ContentPackage.LocalModsDir, StringComparison.InvariantCultureIgnoreCase)) { string modName = subDirs[1]; - ContentPackage contentPackage = ContentPackage.AllPackages.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); + ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); if (contentPackage != null) { - Steamworks.Data.PublishedFileId packageId = contentPackage.SteamWorkshopId; - - Task itemInfoTask = Steamworks.Ugc.Item.GetAsync(packageId); - Task itemUpdateTask = Task.Run(async () => - { - while (!itemInfoTask.IsCompleted) - { - Steamworks.SteamClient.RunCallbacks(); - await Task.Delay(16); - } - return itemInfoTask.Result; - }); - - Steamworks.Ugc.Item? item = itemUpdateTask.Result; - if (item?.Owner.Id == Steam.SteamManager.GetSteamID()) - { - forceToSubFolder = false; - string targetPath = Path.Combine(prevDir, savePath).CleanUpPath(); - if (!contentPackage.Files.Any(f => f.Type == ContentType.Submarine && - f.Path.CleanUpPath().Equals(targetPath, StringComparison.InvariantCultureIgnoreCase))) - { - contentPackage.AddFile(new ContentFile(targetPath, ContentType.Submarine)); - } - contentPackage.Save(contentPackage.Path, reload: false); - } + modProject = new ModProject(contentPackage); + fileListPath = contentPackage.Path; + EnqueueForReload(contentPackage); } } - savePath = Path.Combine(forceToSubFolder && !isInSubFolder ? SubmarineInfo.SavePath : prevDir, savePath).CleanUpPath(); + + savePath = Path.Combine(prevDir, savePath).CleanUpPath(); + if (!isVanillaSub) + { + addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); + } } else { - savePath = Path.Combine(SubmarineInfo.SavePath, savePath); + savePath = Path.Combine(newLocalModDir, savePath); + ModProject modProject = new ModProject() { Name = name }; + addSubAndSaveModProject(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } + savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); -#if !DEBUG - var vanilla = GameMain.VanillaContent; +#if !DEBUG if (vanilla != null) { - var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); string pathToCompare = savePath.Replace(@"\", @"/"); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) + if (vanillaSubs.Any(sub => sub.Value.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) { - GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } } #endif - if (Submarine.MainSub != null) + if (MainSub != null) { Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && Submarine.MainSub.Info.Type != SubmarineType.OutpostModule) + if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule) { bool savePreviewImage = true; using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); @@ -1890,21 +1935,30 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Saving the preview image of the submarine \"{Submarine.MainSub.Info.Name}\" failed.", e); + DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e); savePreviewImage = false; } - Submarine.MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); + MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); } else { - Submarine.MainSub.TrySaveAs(savePath); + MainSub.TrySaveAs(savePath); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; - Submarine.MainSub.CheckForErrors(); + MainSub.CheckForErrors(); - GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green); + if (savePath.StartsWith(newLocalModDir)) + { + ContentPackageManager.LocalPackages.Refresh(); + var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir)); + if (newPackage is RegularPackage regular) + { + ContentPackageManager.EnabledPackages.EnableRegular(regular); + } + } SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } @@ -1916,7 +1970,7 @@ namespace Barotrauma if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } linkedSubBox.AddItem(sub.Name, sub); } - subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } return false; @@ -1942,7 +1996,7 @@ namespace Barotrauma var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.6f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; - //var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveSubDialogHeader"), font: GUI.LargeFont); + //var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveSubDialogHeader"), font: GUIStyle.LargeFont); var columnArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), paddedSaveFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.55f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.01f, Stretch = true }; @@ -1952,7 +2006,7 @@ namespace Barotrauma var nameHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), true); var saveSubLabel = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), - TextManager.Get("SaveSubDialogName"), font: GUI.SubHeadingFont); + TextManager.Get("SaveSubDialogName"), font: GUIStyle.SubHeadingFont); submarineNameCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); @@ -1965,7 +2019,7 @@ namespace Barotrauma if (text.Length > submarineNameLimit) { nameBox.Text = text.Substring(0, submarineNameLimit); - nameBox.Flash(GUI.Style.Red); + nameBox.Flash(GUIStyle.Red); return true; } @@ -1973,18 +2027,18 @@ namespace Barotrauma return true; }; - nameBox.Text = subNameLabel?.Text ?? ""; + nameBox.Text = subNameLabel?.Text?.SanitizedValue ?? ""; submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + submarineNameLimit; var descriptionHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), isHorizontal: true); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), descriptionHeaderGroup.RectTransform), TextManager.Get("SaveSubDialogDescription"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), descriptionHeaderGroup.RectTransform), TextManager.Get("SaveSubDialogDescription"), font: GUIStyle.SubHeadingFont); submarineDescriptionCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), descriptionHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform)); descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center), - font: GUI.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) + font: GUIStyle.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) { Padding = new Vector4(10 * GUI.Scale) }; @@ -1994,7 +2048,7 @@ namespace Barotrauma if (text.Length > submarineDescriptionLimit) { descriptionBox.Text = text.Substring(0, submarineDescriptionLimit); - descriptionBox.Flash(GUI.Style.Red); + descriptionBox.Flash(GUIStyle.Red); return true; } @@ -2046,13 +2100,13 @@ namespace Barotrauma var outpostModuleGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft); - HashSet availableFlags = new HashSet(); - foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } - foreach (string flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + HashSet availableFlags = new HashSet(); + foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (Identifier flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) + foreach (Identifier flag in sub.OutpostModuleInfo.ModuleFlags) { if (flag == "none") { continue; } availableFlags.Add(flag); @@ -2060,22 +2114,22 @@ namespace Barotrauma } var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), - text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s)) ?? "None".ToEnumerable()), selectMultiple: true); - foreach (string flag in availableFlags) + text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); + foreach (Identifier flag in availableFlags) { - moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } - if (Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains(flag)) + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); + if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains(flag)) { moduleTypeDropDown.SelectItem(flag); } } moduleTypeDropDown.OnSelected += (_, __) => { - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); + if (MainSub?.Info?.OutpostModuleInfo == null) { return false; } + MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); moduleTypeDropDown.Text = ToolBox.LimitString( - Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", + MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", moduleTypeDropDown.Font, moduleTypeDropDown.Rect.Width); return true; }; @@ -2088,30 +2142,30 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), TextManager.Get("outpostmoduleallowattachto"), textAlignment: Alignment.CenterLeft); var allowAttachDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), - text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s)) ?? "Any".ToEnumerable()), selectMultiple: true); + text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"Any").ToEnumerable()), selectMultiple: true); allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any"); - if (Submarine.MainSub.Info.OutpostModuleInfo == null || - !Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || - Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))) + if (MainSub.Info.OutpostModuleInfo == null || + !MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || + MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")) { allowAttachDropDown.SelectItem("any"); } - foreach (string flag in availableFlags) + foreach (Identifier flag in availableFlags) { - if (flag.Equals("any", StringComparison.OrdinalIgnoreCase) || flag.Equals("none", StringComparison.OrdinalIgnoreCase)) { continue; } - allowAttachDropDown.AddItem(TextManager.Capitalize(flag), flag); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } - if (Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Contains(flag)) + if (flag == "any" || flag == "none") { continue; } + allowAttachDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); + if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Contains(flag)) { allowAttachDropDown.SelectItem(flag); } } allowAttachDropDown.OnSelected += (_, __) => { - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.SetAllowAttachTo(allowAttachDropDown.SelectedDataMultiple.Cast()); + if (MainSub?.Info?.OutpostModuleInfo == null) { return false; } + MainSub.Info.OutpostModuleInfo.SetAllowAttachTo(allowAttachDropDown.SelectedDataMultiple.Cast()); allowAttachDropDown.Text = ToolBox.LimitString( - Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? allowAttachDropDown.Text : "None", + MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? allowAttachDropDown.Text.Value : "None", allowAttachDropDown.Font, allowAttachDropDown.Rect.Width); return true; }; @@ -2122,26 +2176,26 @@ namespace Barotrauma var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any" }; - foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier.ToLowerInvariant()); } + HashSet availableLocationTypes = new HashSet { "any".ToIdentifier() }; + foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), - text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); - foreach (string locationType in availableLocationTypes) + text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true); + foreach (Identifier locationType in availableLocationTypes) { - locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } - if (Submarine.MainSub.Info.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType)) + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType); + if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (MainSub.Info.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType)) { locationTypeDropDown.SelectItem(locationType); } } - if (!Submarine.MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any"); } + if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any"); } locationTypeDropDown.OnSelected += (_, __) => { - Submarine.MainSub?.Info?.OutpostModuleInfo?.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); - locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); + MainSub?.Info?.OutpostModuleInfo?.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); + locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text.Value, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); return true; }; locationTypeGroup.RectTransform.MinSize = new Point(0, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); @@ -2156,12 +2210,12 @@ namespace Barotrauma var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); - var outpostModuleInfo = Submarine.MainSub.Info?.OutpostModuleInfo; + var outpostModuleInfo = MainSub.Info?.OutpostModuleInfo; if (outpostModuleInfo != null) { if (outpostModuleInfo.GapPositions == OutpostModuleInfo.GapPosition.None) { - outpostModuleInfo.DetermineGapPositions(Submarine.MainSub); + outpostModuleInfo.DetermineGapPositions(MainSub); } foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) { @@ -2176,14 +2230,14 @@ namespace Barotrauma gapPositionDropDown.OnSelected += (_, __) => { - if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.GapPositions = OutpostModuleInfo.GapPosition.None; + if (MainSub.Info?.OutpostModuleInfo == null) { return false; } + MainSub.Info.OutpostModuleInfo.GapPositions = OutpostModuleInfo.GapPosition.None; if (gapPositionDropDown.SelectedDataMultiple.Any()) { - List gapPosTexts = new List(); + List gapPosTexts = new List(); foreach (OutpostModuleInfo.GapPosition gapPos in gapPositionDropDown.SelectedDataMultiple) { - Submarine.MainSub.Info.OutpostModuleInfo.GapPositions |= gapPos; + MainSub.Info.OutpostModuleInfo.GapPositions |= gapPos; gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString())); } gapPositionDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), gapPositionDropDown.Font, gapPositionDropDown.Rect.Width); @@ -2210,12 +2264,12 @@ namespace Barotrauma new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxModuleCountGroup.RectTransform), GUINumberInput.NumberType.Int) { ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip"), - IntValue = Submarine.MainSub?.Info?.OutpostModuleInfo?.MaxCount ?? 1000, + IntValue = MainSub?.Info?.OutpostModuleInfo?.MaxCount ?? 1000, MinValueInt = 0, MaxValueInt = 1000, OnValueChanged = (numberInput) => { - Submarine.MainSub.Info.OutpostModuleInfo.MaxCount = numberInput.IntValue; + MainSub.Info.OutpostModuleInfo.MaxCount = numberInput.IntValue; } }; @@ -2227,12 +2281,12 @@ namespace Barotrauma TextManager.Get("subeditor.outpostcommonness"), textAlignment: Alignment.CenterLeft, wrap: true); new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), GUINumberInput.NumberType.Float) { - FloatValue = Submarine.MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, + FloatValue = MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, MinValueFloat = 0, MaxValueFloat = 100, OnValueChanged = (numberInput) => { - Submarine.MainSub.Info.OutpostModuleInfo.Commonness = numberInput.FloatValue; + MainSub.Info.OutpostModuleInfo.Commonness = numberInput.FloatValue; } }; outpostSettingsContainer.RectTransform.MinSize = new Point(0, outpostSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); @@ -2255,20 +2309,20 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), priceGroup.RectTransform), TextManager.Get("subeditor.price"), textAlignment: Alignment.CenterLeft, wrap: true); - int basePrice = (GameMain.DebugDraw ? 0 : Submarine.MainSub?.CalculateBasePrice()) ?? 1000; + int basePrice = (GameMain.DebugDraw ? 0 : MainSub?.CalculateBasePrice()) ?? 1000; new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) { - IntValue = Math.Max(Submarine.MainSub?.Info?.Price ?? basePrice, basePrice), + IntValue = Math.Max(MainSub?.Info?.Price ?? basePrice, basePrice), MinValueInt = basePrice, MaxValueInt = 999999, OnValueChanged = (numberInput) => { - Submarine.MainSub.Info.Price = numberInput.IntValue; + MainSub.Info.Price = numberInput.IntValue; } }; - if (Submarine.MainSub?.Info != null) + if (MainSub?.Info != null) { - Submarine.MainSub.Info.Price = Math.Max(Submarine.MainSub.Info.Price, basePrice); + MainSub.Info.Price = Math.Max(MainSub.Info.Price, basePrice); } var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -2287,11 +2341,11 @@ namespace Barotrauma classDropDown.OnSelected += (selected, userdata) => { SubmarineClass submarineClass = (SubmarineClass)userdata; - Submarine.MainSub.Info.SubmarineClass = submarineClass; + MainSub.Info.SubmarineClass = submarineClass; return true; }; - classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); - classText.Enabled = classDropDown.ButtonEnabled = !Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle); + classDropDown.SelectItem(MainSub.Info.SubmarineClass); + classText.Enabled = classDropDown.ButtonEnabled = !MainSub.Info.HasTag(SubmarineTag.Shuttle); var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { @@ -2300,7 +2354,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewSizeArea.RectTransform), - TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUI.SmallFont); + TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), GUINumberInput.NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, @@ -2316,15 +2370,15 @@ namespace Barotrauma crewSizeMin.OnValueChanged += (numberInput) => { crewSizeMax.IntValue = Math.Max(crewSizeMax.IntValue, numberInput.IntValue); - Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; + MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; crewSizeMax.OnValueChanged += (numberInput) => { crewSizeMin.IntValue = Math.Min(crewSizeMin.IntValue, numberInput.IntValue); - Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; + MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -2334,52 +2388,53 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewExpArea.RectTransform), - TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUI.SmallFont); + TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); var toggleExpLeft = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleLeft"); - var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform), crewExperienceLevels[0], textAlignment: Alignment.Center); + var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform), + text: crewExperienceLevels[0], textAlignment: Alignment.Center); var toggleExpRight = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleRight"); toggleExpLeft.OnClicked += (btn, userData) => { - int currentIndex = Array.IndexOf(crewExperienceLevels, (string)experienceText.UserData); + int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); currentIndex--; if (currentIndex < 0) currentIndex = crewExperienceLevels.Length - 1; experienceText.UserData = crewExperienceLevels[currentIndex]; experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; toggleExpRight.OnClicked += (btn, userData) => { - int currentIndex = Array.IndexOf(crewExperienceLevels, (string)experienceText.UserData); + int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); currentIndex++; if (currentIndex >= crewExperienceLevels.Length) currentIndex = 0; experienceText.UserData = crewExperienceLevels[currentIndex]; experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; - if (Submarine.MainSub != null) + if (MainSub != null) { - int min = Submarine.MainSub.Info.RecommendedCrewSizeMin; - int max = Submarine.MainSub.Info.RecommendedCrewSizeMax; + int min = MainSub.Info.RecommendedCrewSizeMin; + int max = MainSub.Info.RecommendedCrewSizeMax; crewSizeMin.IntValue = min; crewSizeMax.IntValue = max; - experienceText.UserData = string.IsNullOrEmpty(Submarine.MainSub.Info.RecommendedCrewExperience) ? - crewExperienceLevels[0] : Submarine.MainSub.Info.RecommendedCrewExperience; + experienceText.UserData = string.IsNullOrEmpty(MainSub.Info.RecommendedCrewExperience) ? + crewExperienceLevels[0] : MainSub.Info.RecommendedCrewExperience; experienceText.Text = TextManager.Get((string)experienceText.UserData); } subTypeDropdown.OnSelected += (selected, userdata) => { SubmarineType type = (SubmarineType)userdata; - Submarine.MainSub.Info.Type = type; + MainSub.Info.Type = type; if (type == SubmarineType.OutpostModule) { - Submarine.MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(Submarine.MainSub.Info); + MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(MainSub.Info); } previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; @@ -2392,11 +2447,11 @@ namespace Barotrauma subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); // right column --------------------------------------------------- - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUI.SubHeadingFont); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUIStyle.SubHeadingFont); var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; - previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.Info.PreviewImage, scaleToFit: true); + previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), MainSub?.Info.PreviewImage, scaleToFit: true); previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; @@ -2408,9 +2463,9 @@ namespace Barotrauma { CreateImage(defaultPreviewImageSize.X, defaultPreviewImageSize.Y, imgStream); previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream, compress: false), null, null); - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; + MainSub.Info.PreviewImage = previewImage.Sprite; } } return true; @@ -2430,9 +2485,9 @@ namespace Barotrauma } previewImage.Sprite = new Sprite(file, sourceRectangle: null); - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; + MainSub.Info.PreviewImage = previewImage.Sprite; } }; FileSelection.ClearFileTypeFilters(); @@ -2450,7 +2505,7 @@ namespace Barotrauma var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), rightColumn.RectTransform), style: null); var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), - TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUI.SmallFont); + TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUIStyle.SmallFont); var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomLeft), @@ -2458,15 +2513,15 @@ namespace Barotrauma foreach (SubmarineTag tag in Enum.GetValues(typeof(SubmarineTag))) { - string tagStr = TextManager.Get(tag.ToString()); + LocalizedString tagStr = TextManager.Get(tag.ToString()); var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), - tagStr, font: GUI.SmallFont) + tagStr, font: GUIStyle.SmallFont) { - Selected = Submarine.MainSub != null && Submarine.MainSub.Info.HasTag(tag), + Selected = MainSub != null && MainSub.Info.HasTag(tag), UserData = tag, OnSelected = (GUITickBox tickBox) => { - if (Submarine.MainSub == null) return false; + if (MainSub == null) return false; SubmarineTag tag = (SubmarineTag)tickBox.UserData; if (tag == SubmarineTag.Shuttle) { @@ -2476,17 +2531,17 @@ namespace Barotrauma } else { - classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); + classDropDown.SelectItem(MainSub.Info.SubmarineClass); } classText.Enabled = classDropDown.ButtonEnabled = !tickBox.Selected; } if (tickBox.Selected) { - Submarine.MainSub.Info.AddTag(tag); + MainSub.Info.AddTag(tag); } else { - Submarine.MainSub.Info.RemoveTag(tag); + MainSub.Info.RemoveTag(tag); } return true; } @@ -2494,37 +2549,38 @@ namespace Barotrauma } var contentPackagesLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform, Anchor.TopRight), - TextManager.Get("RequiredContentPackages"), wrap: true, font: GUI.SmallFont); + TextManager.Get("RequiredContentPackages"), wrap: true, font: GUIStyle.SmallFont); var contentPackList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - contentPackagesLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomRight)); - if (Submarine.MainSub != null) { - List contentPacks = Submarine.MainSub.Info.RequiredContentPackages.ToList(); - foreach (ContentPackage contentPack in ContentPackage.AllPackages) + if (MainSub != null) + { + List contentPacks = MainSub.Info.RequiredContentPackages.ToList(); + foreach (ContentPackage contentPack in ContentPackageManager.AllPackages) { //don't show content packages that only define submarine files //(it doesn't make sense to require another sub to be installed to install this one) - if (contentPack.Files.All(cp => cp.Type == ContentType.Submarine)) { continue; } + if (contentPack.Files.All(f => f is SubmarineFile)) { continue; } if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } } foreach (string contentPackageName in contentPacks) { - var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUI.SmallFont) + var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) { - Selected = Submarine.MainSub.Info.RequiredContentPackages.Contains(contentPackageName), + Selected = MainSub.Info.RequiredContentPackages.Contains(contentPackageName), UserData = contentPackageName }; cpTickBox.OnSelected += tickBox => { if (tickBox.Selected) { - Submarine.MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); + MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); } else { - Submarine.MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); + MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); } return true; }; @@ -2557,10 +2613,10 @@ namespace Barotrauma subSettingsContainer.Recalculate(); outpostSettingsContainer.Recalculate(); - descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Info.Description; + descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; - subTypeDropdown.SelectItem(Submarine.MainSub.Info.Type); + subTypeDropdown.SelectItem(MainSub.Info.Type); if (quickSave) { SaveSub(saveButton, saveButton.UserData); } } @@ -2584,7 +2640,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), - TextManager.Get("SaveItemAssemblyDialogHeader"), font: GUI.LargeFont); + TextManager.Get("SaveItemAssemblyDialogHeader"), font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveItemAssemblyDialogName")); nameBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 0.1f), paddedSaveFrame.RectTransform)); @@ -2598,7 +2654,7 @@ namespace Barotrauma var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), paddedSaveFrame.RectTransform)); descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.TopLeft), - font: GUI.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) + font: GUIStyle.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) { Padding = new Vector4(10 * GUI.Scale) }; @@ -2639,7 +2695,7 @@ namespace Barotrauma /// private List LoadItemAssemblyInventorySafe(ItemAssemblyPrefab assemblyPrefab) { - var realItems = assemblyPrefab.CreateInstance(Vector2.Zero, Submarine.MainSub); + var realItems = assemblyPrefab.CreateInstance(Vector2.Zero, MainSub); var itemInstance = new List(); realItems.ForEach(entity => { @@ -2655,7 +2711,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(nameBox.Text)) { - GUI.AddMessage(TextManager.Get("ItemAssemblyNameMissingWarning"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("ItemAssemblyNameMissingWarning"), GUIStyle.Red); nameBox.Flash(); return false; @@ -2665,7 +2721,7 @@ namespace Barotrauma { if (nameBox.Text.Contains(illegalChar)) { - GUI.AddMessage(TextManager.GetWithVariable("ItemAssemblyNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("ItemAssemblyNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); nameBox.Flash(); return false; } @@ -2675,28 +2731,15 @@ namespace Barotrauma #if DEBUG string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; #else - string saveFolder = ItemAssemblyPrefab.SaveFolder; - if (!Directory.Exists(saveFolder)) - { - try - { - Directory.CreateDirectory(saveFolder); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to create a directory for the item assmebly.", e); - return false; - } - } + string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); #endif - string filePath = Path.Combine(saveFolder, nameBox.Text + ".xml"); + string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); if (File.Exists(filePath)) { var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { msgBox.Close(); - ItemAssemblyPrefab.Remove(filePath); Save(); return true; }; @@ -2706,7 +2749,7 @@ namespace Barotrauma { var identifier = nameBox.Text.ToLowerInvariant().Replace(" ", ""); var existingPrefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false); - if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath) == ItemAssemblyPrefab.VanillaSaveFolder) + if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath.Value) == ItemAssemblyPrefab.VanillaSaveFolder) { var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyVanillaFileExistsWarning")); } @@ -2724,7 +2767,20 @@ namespace Barotrauma #else doc.SaveSafe(filePath); #endif - new ItemAssemblyPrefab(filePath, allowOverwrite: true); + ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); + if (existingContentPackage == null) + { + //content package doesn't exist, create one + ModProject modProject = new ModProject() { Name = nameBox.Text }; + var newFile = ModProject.File.FromPath(filePath); + modProject.AddFile(newFile); + ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); + } + else + { + EnqueueForReload(existingContentPackage); + } + UpdateEntityList(); } @@ -2735,16 +2791,21 @@ namespace Barotrauma private void SnapToGrid() { // First move components - foreach (Item item in MapEntity.SelectedList.Where(entity => entity is Item).Cast()) + foreach (MapEntity e in MapEntity.SelectedList) { - var wire = item.GetComponent(); - if (wire == null) + // Items snap to centre of nearest grid square + Vector2 offset = e.Position; + offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + if (e is Item item) { - // Items snap to centre of nearest grid square - Vector2 offset = item.Position; - offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + var wire = item.GetComponent(); + if (wire != null) { continue; } item.Move(offset); } + else if (e is Structure structure) + { + structure.Move(offset); + } } // Then move wires, separated as moving components also moves the start and end node of wires @@ -2786,9 +2847,9 @@ namespace Barotrauma Stretch = true }; - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUI.Font, createClearButton: true); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUIStyle.Font, createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchBox.RectTransform), TextManager.Get("serverlog.filter"), - textAlignment: Alignment.CenterLeft, font: GUI.Font) + textAlignment: Alignment.CenterLeft, font: GUIStyle.Font) { CanBeFocused = false, IgnoreLayoutGroups = true @@ -2832,7 +2893,7 @@ namespace Barotrauma textTag = "MissionType.Pirate"; } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, - TextManager.Get(textTag), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") + TextManager.Get(textTag), font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") { CanBeFocused = false }; @@ -2840,7 +2901,7 @@ namespace Barotrauma } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(sub.Name, GUI.Font, subList.Rect.Width - 80)) + ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80)) { UserData = sub, ToolTip = sub.FilePath @@ -2849,19 +2910,19 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.Shuttle)) { var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = textBlock.TextColor * 0.8f, - ToolTip = textBlock.RawToolTip + ToolTip = textBlock.ToolTip.SanitizedString }; } else if (sub.IsPlayer) { var classText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = textBlock.TextColor * 0.8f, - ToolTip = textBlock.RawToolTip + ToolTip = textBlock.ToolTip.SanitizedString }; } } @@ -2901,22 +2962,13 @@ namespace Barotrauma DateTime time = DateTime.MinValue.AddSeconds(saveElement.GetAttributeUInt64("time", 0)); TimeSpan difference = DateTime.UtcNow - time; - string tooltip = TextManager.GetWithVariables("subeditor.autosaveage", - new[] - { - "[hours]", - "[minutes]", - "[seconds]" - }, - new[] - { - ((int)Math.Floor(difference.TotalHours)).ToString(), - difference.Minutes.ToString(), - difference.Seconds.ToString() - }); + LocalizedString tooltip = TextManager.GetWithVariables("subeditor.autosaveage", + ("[hours]", ((int)Math.Floor(difference.TotalHours)).ToString()), + ("[minutes]", difference.Minutes.ToString()), + ("[seconds]", difference.Seconds.ToString())); - string submarineName = saveElement.GetAttributeString("name", TextManager.Get("UnspecifiedSubFileName")); - string timeFormat; + string submarineName = saveElement.GetAttributeString("name", TextManager.Get("UnspecifiedSubFileName").Value); + LocalizedString timeFormat; double totalMinutes = difference.TotalMinutes; @@ -2933,7 +2985,7 @@ namespace Barotrauma timeFormat = TextManager.GetWithVariable("subeditor.saveageminutes", "[minutes]", difference.Minutes.ToString()); } - string entryName = TextManager.GetWithVariables("subeditor.autosaveentry", new []{ "[submarine]", "[saveage]" }, new []{ submarineName, timeFormat }); + LocalizedString entryName = TextManager.GetWithVariables("subeditor.autosaveentry", ("[submarine]", submarineName), ("[saveage]", timeFormat)); loadAutoSave.AddItem(entryName, saveElement, tooltip); } @@ -2977,14 +3029,15 @@ namespace Barotrauma { if (!(UserData is XElement element)) { return; } - string filePath = element.GetAttributeString("file", ""); + #warning TODO: revise + string filePath = element.GetAttributeStringUnrestricted("file", ""); if (string.IsNullOrWhiteSpace(filePath)) { return; } var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); // set the submarine file path to the "default" value loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); - loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName"); + loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName").Value; try { loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); @@ -2993,15 +3046,15 @@ namespace Barotrauma { DebugConsole.ThrowError("Failed to find a name for the submarine.", e); } - Submarine.MainSub = loadedSub; - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.UpdateTransform(); - Submarine.MainSub.Info.Name = loadedSub.Info.Name; + MainSub = loadedSub; + MainSub.SetPrevTransform(MainSub.Position); + MainSub.UpdateTransform(); + MainSub.Info.Name = loadedSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); CreateDummyCharacter(); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + cam.Position = MainSub.Position + MainSub.HiddenSubPosition; loadFrame = null; } @@ -3033,15 +3086,15 @@ namespace Barotrauma { Submarine.Unload(); var selectedSub = new Submarine(info); - Submarine.MainSub = selectedSub; - Submarine.MainSub.UpdateTransform(interpolate: false); + MainSub = selectedSub; + MainSub.UpdateTransform(interpolate: false); ClearUndoBuffer(); CreateDummyCharacter(); - string name = Submarine.MainSub.Info.Name; + string name = MainSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + cam.Position = MainSub.Position + MainSub.HiddenSubPosition; loadFrame = null; @@ -3067,31 +3120,38 @@ namespace Barotrauma ReconstructLayers(); } + private RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) + { + foreach (RegularPackage regularPackage in ContentPackageManager.RegularPackages) + { + if (regularPackage.Files.Length == 1 && regularPackage.Files[0].Path == sub.FilePath) + { + return regularPackage; + } + } + + return null; + } + private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } - //if the sub is included in a content package that only defines that one sub, - //delete the content package as well - ContentPackage subPackage = null; - foreach (ContentPackage cp in ContentPackage.RegularPackages) - { - if (cp.Files.Count == 1 && Path.GetFullPath(cp.Files[0].Path) == Path.GetFullPath(sub.FilePath)) - { - subPackage = cp; - break; - } - } - subPackage?.Delete(); - + //If the sub is included in a content package that only defines that one sub, + //check that it's a local content package and only allow deletion if it is. + var subPackage = GetContentPackageIntrinsicallyTiedToSub(sub); + if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } + var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked += (btn, userData) => { try { + Directory.Delete(Path.GetDirectoryName(subPackage.Path), true); + sub.Dispose(); File.Delete(sub.FilePath); SubmarineInfo.RefreshSavedSubs(); @@ -3116,7 +3176,7 @@ namespace Barotrauma categoryButton.UserData == null; string categoryName = entityCategory.HasValue ? entityCategory.Value.ToString() : "All"; selectedCategoryText.Text = TextManager.Get("MapEntityCategory." + categoryName); - selectedCategoryButton.ApplyStyle(GUI.Style.GetComponentStyle("CategoryButton." + categoryName)); + selectedCategoryButton.ApplyStyle(GUIStyle.GetComponentStyle("CategoryButton." + categoryName)); } selectedCategory = entityCategory; @@ -3166,7 +3226,7 @@ namespace Barotrauma var innerList = child.GetChild(); foreach (GUIComponent grandChild in innerList.Content.Children) { - grandChild.Visible = ((MapEntityPrefab)grandChild.UserData).Name.ToLower().Contains(filter); + grandChild.Visible = ((MapEntityPrefab)grandChild.UserData).Name.Value.Contains(filter, StringComparison.OrdinalIgnoreCase); } }; categorizedEntityList.UpdateScrollBarSize(); @@ -3181,7 +3241,7 @@ namespace Barotrauma { child.Visible = (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) && - ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); + ((MapEntityPrefab)child.UserData).Name.Value.Contains(filter, StringComparison.OrdinalIgnoreCase); } allEntityList.UpdateScrollBarSize(); allEntityList.BarScroll = 0.0f; @@ -3263,7 +3323,7 @@ namespace Barotrauma new ContextMenuOption("Editor.SelectSame", isEnabled: hasTargets, onSelected: delegate { bool doorGapSelected = targets.Any(t => t is Gap gap && gap.ConnectedDoor != null); - foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) + foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.Prefab != null && targets.Any(t => t.Prefab?.Identifier == e.Prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) { if (MapEntity.SelectedList.Contains(match)) { continue; } if (match is Gap gap) @@ -3287,7 +3347,7 @@ namespace Barotrauma new ContextMenuOption("SubEditor.ToggleImageEditing", isEnabled: true, onSelected: delegate { ImageManager.EditorMode = !ImageManager.EditorMode; - if (!ImageManager.EditorMode) { GameMain.Config.SaveNewPlayerConfig(); } + if (!ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); } })); } else @@ -3353,7 +3413,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(name)) { - name = TextManager.Get("editor.layer.newlayer"); + name = TextManager.Get("editor.layer.newlayer").Value; } string incrementedName = name; @@ -3433,7 +3493,7 @@ namespace Barotrauma return; } - Submarine sub = Submarine.MainSub; + Submarine sub = MainSub; List entities; try { @@ -3473,7 +3533,7 @@ namespace Barotrauma } else if (selectedEntity is { SerializableProperties: { } props} ) { - if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) + if (props.TryGetValue(property.Name.ToIdentifier(), out SerializableProperty foundProp)) { entities.Add((selectedEntity, (Color) foundProp.GetValue(selectedEntity), foundProp)); } @@ -3490,14 +3550,14 @@ namespace Barotrauma Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.4f : 0.3f, 0.3f); - GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) + GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) { UserData = "colorpicker", Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform)); - GUITextBlock headerText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), contentLayout.RectTransform), property.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.TopCenter) + GUITextBlock headerText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), contentLayout.RectTransform), property.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopCenter) { AutoScaleVertical = true }; @@ -3528,17 +3588,17 @@ namespace Barotrauma float currentHue = colorPicker.SelectedHue / 360f; GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; + new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue }; GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; + new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation }; GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; + new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue }; GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; @@ -3594,7 +3654,7 @@ namespace Barotrauma } List affected = entities.Select(t => t.Entity).Where(se => se is MapEntity { Removed: false } || se is ItemComponent).ToList(); - StoreCommand(new PropertyCommand(affected, property.Name, newColor, oldProperties)); + StoreCommand(new PropertyCommand(affected, property.Name.ToIdentifier(), newColor, oldProperties)); if (MapEntity.EditingHUD != null && (MapEntity.EditingHUD.UserData == entity || (!(entity is ItemComponent ic) || MapEntity.EditingHUD.UserData == ic.Item))) { @@ -3605,7 +3665,7 @@ namespace Barotrauma SerializableEntityEditor.LockEditing = true; foreach (SerializableEntityEditor editor in editors) { - if (editor.UserData == entity && editor.Fields.TryGetValue(property.Name, out GUIComponent[] _)) + if (editor.UserData == entity && editor.Fields.TryGetValue(property.Name.ToIdentifier(), out GUIComponent[] _)) { editor.UpdateValue(property, newColor, flash: false); } @@ -3725,7 +3785,7 @@ namespace Barotrauma foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (string.IsNullOrEmpty(itemPrefab.Name)) { continue; } + if (itemPrefab.Name.IsNullOrEmpty()) { continue; } if (!itemPrefab.Tags.Contains("wire")) { continue; } GUIFrame imgFrame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, listBox.Rect.Width / 2), listBox.Content.RectTransform), style: "ListBoxElement") @@ -3733,7 +3793,7 @@ namespace Barotrauma UserData = itemPrefab }; - var img = new GUIImage(new RectTransform(new Vector2(0.9f), imgFrame.RectTransform, Anchor.Center), itemPrefab.sprite, scaleToFit: true) + var img = new GUIImage(new RectTransform(new Vector2(0.9f), imgFrame.RectTransform, Anchor.Center), itemPrefab.Sprite, scaleToFit: true) { UserData = itemPrefab, Color = itemPrefab.SpriteColor @@ -3855,25 +3915,25 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(text)) { - textBox.Flash(GUI.Style.Red); + textBox.Flash(GUIStyle.Red); return false; } - if (Submarine.MainSub != null) Submarine.MainSub.Info.Name = text; + if (MainSub != null) MainSub.Info.Name = text; textBox.Deselect(); textBox.Text = text; - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; } private void ChangeSubDescription(GUITextBox textBox, string text) { - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.Description = text; + MainSub.Info.Description = text; } else { @@ -3901,7 +3961,7 @@ namespace Barotrauma showEntitiesPanel.Visible = true; showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(entityCountPanel.Rect.Right, saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); matchingTickBox.Selected = true; - matchingTickBox.Flash(GUI.Style.Green); + matchingTickBox.Flash(GUIStyle.Green); } } @@ -3943,7 +4003,7 @@ namespace Barotrauma } case ItemPrefab itemPrefab when PlayerInput.IsShiftDown(): { - var item = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + var item = new Item(itemPrefab, Vector2.Zero, MainSub); if (!inv.TryPutItem(item, dummyCharacter)) { // We failed, remove the item so it doesn't stay at x:0,y:0 @@ -3983,8 +4043,8 @@ namespace Barotrauma private bool GenerateWaypoints() { - if (Submarine.MainSub == null) { return false; } - return WayPoint.GenerateSubWaypoints(Submarine.MainSub); + if (MainSub == null) { return false; } + return WayPoint.GenerateSubWaypoints(MainSub); } private void AddPreviouslyUsed(MapEntityPrefab mapEntityPrefab) @@ -4002,7 +4062,7 @@ namespace Barotrauma if (existing != null) { previouslyUsedList.Content.RemoveChild(existing); } var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), previouslyUsedList.Content.RectTransform) { MinSize = new Point(0, 15) }, - ToolBox.LimitString(mapEntityPrefab.Name, GUI.SmallFont, previouslyUsedList.Content.Rect.Width), font: GUI.SmallFont) + ToolBox.LimitString(mapEntityPrefab.Name.Value, GUIStyle.SmallFont, previouslyUsedList.Content.Rect.Width), font: GUIStyle.SmallFont) { UserData = mapEntityPrefab }; @@ -4295,9 +4355,9 @@ namespace Barotrauma { Rectangle hullRect = rect; hullRect.Y = -hullRect.Y; - Hull newHull = new Hull(MapEntityPrefab.Find(null, "hull"), + Hull newHull = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), hullRect, - Submarine.MainSub); + MainSub); } foreach (MapEntity e in mapEntityList) @@ -4308,7 +4368,7 @@ namespace Barotrauma Rectangle gapRect = e.WorldRect; gapRect.Y -= 8; gapRect.Height = 16; - Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), gapRect); + Gap newGap = new Gap(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), gapRect); } } @@ -4416,7 +4476,7 @@ namespace Barotrauma commandIndex++; // Start removing old commands - if (Commands.Count > Math.Clamp(GameSettings.SubEditorMaxUndoBuffer, 1, 10240)) + if (Commands.Count > Math.Clamp(GameSettings.CurrentConfig.SubEditorUndoBuffer, 1, 10240)) { Commands.First()?.Cleanup(); Commands.RemoveRange(0, 1); @@ -4435,9 +4495,9 @@ namespace Barotrauma layerList.Deselect(); GUILayoutGroup buttonHeaders = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), layerList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft); - new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; - new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; - new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; + new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; foreach (var (layer, (visibility, linkage)) in Layers) { @@ -4499,7 +4559,7 @@ namespace Barotrauma foreach (var child in buttonHeaders.Children) { var btn = child as GUIButton; - string originalBtnText = btn.Text; + string originalBtnText = btn.Text.Value; btn.Text = ToolBox.LimitString(btn.Text, btn.Font, btn.Rect.Width); if (originalBtnText != btn.Text) { @@ -4523,16 +4583,16 @@ namespace Barotrauma for (int i = 0; i < Commands.Count; i++) { Command command = Commands[i]; - string description = command.GetDescription(); + LocalizedString description = command.GetDescription(); CreateTextBlock(description, description, i + 1, command).RectTransform.SetAsFirstChild(); } CreateTextBlock(TextManager.Get("undo.beginning"), TextManager.Get("undo.beginningtooltip"), 0, null); - GUITextBlock CreateTextBlock(string name, string description, int index, Command command) + GUITextBlock CreateTextBlock(LocalizedString name, LocalizedString description, int index, Command command) { return new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), undoBufferList.Content.RectTransform) { MinSize = new Point(0, 15) }, - ToolBox.LimitString(name, GUI.SmallFont, undoBufferList.Content.Rect.Width), font: GUI.SmallFont, textColor: index == commandIndex ? GUI.Style.Green : (Color?) null) + ToolBox.LimitString(name.Value, GUIStyle.SmallFont, undoBufferList.Content.Rect.Width), font: GUIStyle.SmallFont, textColor: index == commandIndex ? GUIStyle.Green : (Color?) null) { UserData = command, ToolTip = description @@ -4627,8 +4687,8 @@ namespace Barotrauma // Move the camera towards to the focus point if (camTargetFocus != Vector2.Zero) { - if (GameMain.Config.KeyBind(InputType.Up).IsDown() || GameMain.Config.KeyBind(InputType.Down).IsDown() || - GameMain.Config.KeyBind(InputType.Left).IsDown() || GameMain.Config.KeyBind(InputType.Right).IsDown()) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up].IsDown() || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down].IsDown() || + GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Left].IsDown() || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Right].IsDown()) { camTargetFocus = Vector2.Zero; } @@ -4746,7 +4806,7 @@ namespace Barotrauma } } - if (GameMain.Config.KeyBind(InputType.ToggleInventory).IsHit() && mode == Mode.Default) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].IsHit() && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); } @@ -4913,7 +4973,7 @@ namespace Barotrauma } cam.TargetPos = Vector2.Zero; - dummyCharacter.Submarine = Submarine.MainSub; + dummyCharacter.Submarine = MainSub; } // Deposit item from our "infinite stack" into inventory slots @@ -4939,7 +4999,7 @@ namespace Barotrauma // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit if (Inventory.IsMouseOnSlot(slot)) { - var newItem = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + var newItem = new Item(itemPrefab, Vector2.Zero, MainSub); if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) { @@ -4963,13 +5023,13 @@ namespace Barotrauma } else { - slot.ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } else { newItem.Remove(); - slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); } if (!newItem.Removed) @@ -5227,12 +5287,12 @@ namespace Barotrauma if (GameMain.DebugDraw) { - GUI.DrawLine(spriteBatch, new Vector2(Submarine.MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(Submarine.MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); - GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -Submarine.MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -Submarine.MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); + GUI.DrawLine(spriteBatch, new Vector2(MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); + GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); } Submarine.DrawBack(spriteBatch, true, e => e is Structure s && - !IsSubcategoryHidden(e.prefab?.Subcategory) && + !IsSubcategoryHidden(e.Prefab?.Subcategory) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); Submarine.DrawPaintedColors(spriteBatch, true); spriteBatch.End(); @@ -5253,15 +5313,15 @@ namespace Barotrauma Submarine.DrawBack(spriteBatch, true, e => (!(e is Structure) || e.SpriteDepth < 0.9f) && - !IsSubcategoryHidden(e.prefab?.Subcategory)); + !IsSubcategoryHidden(e.Prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawDamageable(spriteBatch, null, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); + Submarine.DrawDamageable(spriteBatch, null, editing: true, e => !IsSubcategoryHidden(e.Prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); + Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.Prefab?.Subcategory)); if (!WiringMode && !IsMouseOnEditorGUI()) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); @@ -5291,18 +5351,18 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); - if (Submarine.MainSub != null && cam.Zoom < 5f) + if (MainSub != null && cam.Zoom < 5f) { - Vector2 position = Submarine.MainSub.SubBody != null ? Submarine.MainSub.WorldPosition : Submarine.MainSub.HiddenSubPosition; + Vector2 position = MainSub.SubBody != null ? MainSub.WorldPosition : MainSub.HiddenSubPosition; GUI.DrawIndicator( spriteBatch, position, cam, cam.WorldView.Width, - GUI.SubmarineIcon, Color.LightBlue * 0.5f); + GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.LightBlue * 0.5f); } - var notificationIcon = GUI.Style.GetComponentStyle("GUINotificationButton"); - var tooltipStyle = GUI.Style.GetComponentStyle("GUIToolTip"); + var notificationIcon = GUIStyle.GetComponentStyle("GUINotificationButton"); + var tooltipStyle = GUIStyle.GetComponentStyle("GUIToolTip"); foreach (Gap gap in Gap.GapList) { if (gap.linkedTo.Count == 2 && gap.linkedTo[0] == gap.linkedTo[1]) @@ -5310,7 +5370,7 @@ namespace Barotrauma Vector2 screenPos = Cam.WorldToScreen(gap.WorldPosition); Rectangle rect = new Rectangle(screenPos.ToPoint() - new Point(20), new Point(40)); tooltipStyle.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, Color.White); - notificationIcon.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, GUI.Style.Orange); + notificationIcon.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, GUIStyle.Orange); if (Vector2.Distance(PlayerInput.MousePosition, screenPos) < 30 * Cam.Zoom) { GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("gapinsidehullwarning"), new Rectangle(screenPos.ToPoint(), new Point(10))); @@ -5347,12 +5407,12 @@ namespace Barotrauma } } - GUI.DrawLine(spriteBatch, cam.WorldToScreen(startPos), cam.WorldToScreen(mouseWorldPos), GUI.Style.Green, width: 4); + GUI.DrawLine(spriteBatch, cam.WorldToScreen(startPos), cam.WorldToScreen(mouseWorldPos), GUIStyle.Green, width: 4); decimal realWorldDistance = decimal.Round((decimal) (Vector2.Distance(startPos, mouseWorldPos) * Physics.DisplayToRealWorldRatio), 2); Vector2 offset = new Vector2(GUI.IntScale(24)); - GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUI.Style.TextColor, font: GUI.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 4d71cfc0a..c6d3deeb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -22,15 +22,15 @@ namespace Barotrauma private Submarine? submarine; private Character? dummyCharacter; - public static Effect BlueprintEffect; - private GUIFrame container; + public static Effect BlueprintEffect = null!; + private GUIFrame container = null!; - private TabMenu tabMenu; + private TabMenu? tabMenu; public TestScreen() { Cam = new Camera(); - BlueprintEffect = GameMain.GameScreen.BlueprintEffect; + BlueprintEffect = GameMain.GameScreen.BlueprintEffect!; new GUIButton(new RectTransform(new Point(256, 256), Frame.RectTransform), "Reload shader") { @@ -38,7 +38,7 @@ namespace Barotrauma { BlueprintEffect.Dispose(); GameMain.Instance.Content.Unload(); - BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); + BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl")!; GameMain.GameScreen.BlueprintEffect = BlueprintEffect; return true; } @@ -47,7 +47,7 @@ namespace Barotrauma } public override void Select() - { + { base.Select(); container = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); var tab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); @@ -79,7 +79,7 @@ namespace Barotrauma public override void Update(double deltaTime) { base.Update(deltaTime); - tabMenu.Update(); + tabMenu!.Update(); if (dummyCharacter is { } dummy) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index ab8ef0e8a..0b2da87b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -44,11 +44,11 @@ namespace Barotrauma /// /// Holds the references to the input fields. /// - public Dictionary Fields { get; private set; } = new Dictionary(); + public Dictionary Fields { get; private set; } = new Dictionary(); public void UpdateValue(SerializableProperty property, object newValue, bool flash = true) { - if (!Fields.TryGetValue(property.Name, out GUIComponent[] fields)) + if (!Fields.TryGetValue(property.Name.ToIdentifier(), out GUIComponent[] fields)) { DebugConsole.ThrowError($"No field for {property.Name} found!"); return; @@ -64,7 +64,7 @@ namespace Barotrauma numInput.FloatValue = f; if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -81,7 +81,7 @@ namespace Barotrauma numInput.IntValue = integer; if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -94,7 +94,7 @@ namespace Barotrauma tickBox.Selected = b; if (flash) { - tickBox.Flash(GUI.Style.Green); + tickBox.Flash(GUIStyle.Green); } } } @@ -105,7 +105,7 @@ namespace Barotrauma textBox.Text = s; if (flash) { - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); } } } @@ -116,7 +116,7 @@ namespace Barotrauma dropDown.Select((int)newValue); if (flash) { - dropDown.Flash(GUI.Style.Green); + dropDown.Flash(GUIStyle.Green); } } } @@ -132,7 +132,7 @@ namespace Barotrauma numInput.FloatValue = i == 0 ? v2.X : v2.Y; if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -161,7 +161,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -193,7 +193,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -225,7 +225,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -265,7 +265,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -281,27 +281,27 @@ namespace Barotrauma textBox.Text = a[i]; if (flash) { - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); } } } } } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) : this(parent, entity, inGame ? SerializableProperty.GetProperties(entity).Union(SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? false)) : SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont) { } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) : base(style, new RectTransform(Vector2.One, parent)) { this.elementHeight = (int)(elementHeight * GUI.Scale); - var tickBoxStyle = GUI.Style.GetComponentStyle("GUITickBox"); - var textBoxStyle = GUI.Style.GetComponentStyle("GUITextBox"); - var numberInputStyle = GUI.Style.GetComponentStyle("GUINumberInput"); + var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox"); + var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox"); + var numberInputStyle = GUIStyle.GetComponentStyle("GUINumberInput"); if (tickBoxStyle.Height.HasValue) { this.elementHeight = Math.Max(tickBoxStyle.Height.Value, this.elementHeight); } if (textBoxStyle.Height.HasValue) { this.elementHeight = Math.Max(textBoxStyle.Height.Value, this.elementHeight); } if (numberInputStyle.Height.HasValue) { this.elementHeight = Math.Max(numberInputStyle.Height.Value, this.elementHeight); } @@ -309,7 +309,7 @@ namespace Barotrauma layoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform)) { AbsoluteSpacing = (int)(5 * GUI.Scale) }; if (showName) { - new GUITextBlock(new RectTransform(new Point(layoutGroup.Rect.Width, this.elementHeight), layoutGroup.RectTransform, isFixedSize: true), entity.Name, font: titleFont ?? GUI.Font) + new GUITextBlock(new RectTransform(new Point(layoutGroup.Rect.Width, this.elementHeight), layoutGroup.RectTransform, isFixedSize: true), entity.Name, font: titleFont ?? GUIStyle.Font) { TextColor = Color.White, Color = Color.Black @@ -344,25 +344,24 @@ namespace Barotrauma value = ""; } - string propertyTag = (entity.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); - string fallbackTag = property.PropertyInfo.Name.ToLowerInvariant(); - string displayName = - TextManager.Get($"{propertyTag}", true, useEnglishAsFallBack: false) ?? - TextManager.Get($"sp.{propertyTag}.name", true, useEnglishAsFallBack: false); - if (string.IsNullOrEmpty(displayName)) + Identifier propertyTag = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier(); + Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier(); + LocalizedString displayName = + TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier()); + if (displayName.IsNullOrEmpty()) { Editable editable = property.GetAttribute(); if (editable != null && !string.IsNullOrEmpty(editable.FallBackTextTag)) { - displayName = TextManager.Get(editable.FallBackTextTag, true); + displayName = TextManager.Get(editable.FallBackTextTag); } else { - displayName = TextManager.Get(fallbackTag, true) ?? TextManager.Get($"sp.{fallbackTag}.name", true); + displayName = TextManager.Get(fallbackTag, $"sp.{fallbackTag}.name".ToIdentifier()); } } - if (displayName == null) + if (displayName.IsNullOrEmpty()) { displayName = property.Name.FormatCamelCaseWithSpaces(); #if DEBUG @@ -379,7 +378,7 @@ namespace Barotrauma #endif } - string toolTip = TextManager.Get($"sp.{propertyTag}.description", true, !string.IsNullOrEmpty(fallbackTag) ? $"sp.{fallbackTag}.description" : null); + LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description", $"sp.{fallbackTag}.description"); if (toolTip == null) { @@ -445,20 +444,20 @@ namespace Barotrauma return propertyField; } - public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, string displayName, string toolTip) + public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, LocalizedString displayName, LocalizedString toolTip) { var editableAttribute = property.GetAttribute(); if (editableAttribute.ReadOnly) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; var valueField = new GUITextBlock(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), value.ToString()) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; return valueField; } @@ -466,7 +465,7 @@ namespace Barotrauma { GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Selected = value, ToolTip = toolTip, OnSelected = (tickBox) => @@ -489,15 +488,15 @@ namespace Barotrauma { propertyTickBox.Selected = (bool)property.GetValue(entity); }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyTickBox }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { propertyTickBox }); } return propertyTickBox; } } - public GUIComponent CreateIntField(ISerializableEntity entity, SerializableProperty property, int value, string displayName, string toolTip) + public GUIComponent CreateIntField(ISerializableEntity entity, SerializableProperty property, int value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -508,7 +507,7 @@ namespace Barotrauma var numberInput = new GUITextBlock(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), value.ToString()) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; field = numberInput; } @@ -517,7 +516,7 @@ namespace Barotrauma var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = editableAttribute.MinValueInt; numberInput.MaxValueInt = editableAttribute.MaxValueInt; @@ -535,17 +534,17 @@ namespace Barotrauma }; field = numberInput; } - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { field }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { field }); } return frame; } - public GUIComponent CreateFloatField(ISerializableEntity entity, SerializableProperty property, float value, string displayName, string toolTip) + public GUIComponent CreateFloatField(ISerializableEntity entity, SerializableProperty property, float value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent) { CanBeFocused = false }; - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -554,7 +553,7 @@ namespace Barotrauma Anchor.TopRight), GUINumberInput.NumberType.Float) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; var editableAttribute = property.GetAttribute(); numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -574,14 +573,14 @@ namespace Barotrauma { if (!numberInput.TextBox.Selected) { numberInput.FloatValue = (float)property.GetValue(entity); } }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { numberInput }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { numberInput }); } return frame; } - public GUIComponent CreateEnumField(ISerializableEntity entity, SerializableProperty property, object value, string displayName, string toolTip) + public GUIComponent CreateEnumField(ISerializableEntity entity, SerializableProperty property, object value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -607,14 +606,14 @@ namespace Barotrauma { if (!enumDropDown.Dropped) { enumDropDown.SelectItem(property.GetValue(entity)); } }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { enumDropDown }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { enumDropDown }); } return frame; } - public GUIComponent CreateEnumFlagField(ISerializableEntity entity, SerializableProperty property, object value, string displayName, string toolTip) + public GUIComponent CreateEnumFlagField(ISerializableEntity entity, SerializableProperty property, object value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -645,30 +644,30 @@ namespace Barotrauma return true; }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { enumDropDown }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { enumDropDown }); } return frame; } - public GUIComponent CreateStringField(ISerializableEntity entity, SerializableProperty property, string value, string displayName, string toolTip) + public GUIComponent CreateStringField(ISerializableEntity entity, SerializableProperty property, string value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUILayoutGroup(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont, textAlignment: Alignment.Left) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont, textAlignment: Alignment.Left) { ToolTip = toolTip }; - string translationTextTag = property.GetAttribute()?.translationTextTag; + Identifier translationTextTag = property.GetAttribute()?.TranslationTextTag ?? Identifier.Empty; float browseButtonWidth = 0.1f; var editableAttribute = property.GetAttribute(); float textBoxWidth = inputFieldWidth; - if (translationTextTag != null) { textBoxWidth -= browseButtonWidth; } + if (!translationTextTag.IsEmpty) { textBoxWidth -= browseButtonWidth; } GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(textBoxWidth, 1), frame.RectTransform)) { Enabled = editableAttribute != null && !editableAttribute.ReadOnly, ToolTip = toolTip, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Text = value, OverflowClip = true }; @@ -702,7 +701,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); textBox.Text = (string) property.GetValue(entity); - textBox.Flash(GUI.Style.Green, flashDuration: 1f); + textBox.Flash(GUIStyle.Green, flashDuration: 1f); } //restore the entities that were selected before applying MapEntity.SelectedList.Clear(); @@ -713,23 +712,23 @@ namespace Barotrauma return true; } - if (translationTextTag != null) + if (!translationTextTag.IsEmpty) { new GUIButton(new RectTransform(new Vector2(browseButtonWidth, 1), frame.RectTransform, Anchor.TopRight), "...", style: "GUIButtonSmall") { - OnClicked = (bt, userData) => { CreateTextPicker(translationTextTag, entity, property, propertyBox); return true; } + OnClicked = (bt, userData) => { CreateTextPicker(translationTextTag.Value, entity, property, propertyBox); return true; } }; propertyBox.OnTextChanged += (tb, text) => { - string translatedText = TextManager.Get(text, returnNull: true); - if (translatedText == null) + LocalizedString translatedText = TextManager.Get(text); + if (translatedText.IsNullOrEmpty()) { propertyBox.TextColor = Color.Gray; propertyBox.ToolTip = TextManager.GetWithVariable("StringPropertyCannotTranslate", "[tag]", text ?? string.Empty); } else { - propertyBox.TextColor = GUI.Style.Green; + propertyBox.TextColor = GUIStyle.Green; propertyBox.ToolTip = TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", translatedText); } return true; @@ -737,14 +736,14 @@ namespace Barotrauma propertyBox.Text = value; } frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyBox }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { propertyBox }); } return frame; } - public GUIComponent CreatePointField(ISerializableEntity entity, SerializableProperty property, Point value, string displayName, string toolTip) + public GUIComponent CreatePointField(ISerializableEntity entity, SerializableProperty property, Point value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -759,17 +758,17 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; if (i == 0) @@ -806,14 +805,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateVector2Field(ISerializableEntity entity, SerializableProperty property, Vector2 value, string displayName, string toolTip) + public GUIComponent CreateVector2Field(ISerializableEntity entity, SerializableProperty property, Vector2 value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -828,16 +827,16 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -876,14 +875,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateVector3Field(ISerializableEntity entity, SerializableProperty property, Vector3 value, string displayName, string toolTip) + public GUIComponent CreateVector3Field(ISerializableEntity entity, SerializableProperty property, Vector3 value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -898,17 +897,17 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.33f, 1), inputArea.RectTransform), style: null); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -952,14 +951,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateVector4Field(ISerializableEntity entity, SerializableProperty property, Vector4 value, string displayName, string toolTip) + public GUIComponent CreateVector4Field(ISerializableEntity entity, SerializableProperty property, Vector4 value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -974,17 +973,17 @@ namespace Barotrauma { 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); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -1033,14 +1032,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateColorField(ISerializableEntity entity, SerializableProperty property, Color value, string displayName, string toolTip) + public GUIComponent CreateColorField(ISerializableEntity entity, SerializableProperty property, Color value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform) { MinSize = new Point(80, 26) }, displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform) { MinSize = new Point(80, 26) }, displayName, font: GUIStyle.SmallFont) { ToolTip = displayName + '\n' + toolTip }; @@ -1073,11 +1072,11 @@ namespace Barotrauma { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), element.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), element.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.ColorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = 0; numberInput.MaxValueInt = 255; @@ -1091,7 +1090,7 @@ namespace Barotrauma else numberInput.IntValue = value.A; - numberInput.Font = GUI.SmallFont; + numberInput.Font = GUIStyle.SmallFont; int comp = i; numberInput.OnValueChanged += (numInput) => @@ -1127,14 +1126,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateRectangleField(ISerializableEntity entity, SerializableProperty property, Rectangle value, string displayName, string toolTip) + public GUIComponent CreateRectangleField(ISerializableEntity entity, SerializableProperty property, Rectangle value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = displayName + '\n' + toolTip }; @@ -1148,11 +1147,11 @@ namespace Barotrauma 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: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.RectComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; // Not sure if the min value could in any case be negative. numberInput.MinValueInt = 0; @@ -1199,15 +1198,15 @@ namespace Barotrauma ((GUINumberInput)fields[3]).IntValue = value.Height; } }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateStringArrayField(ISerializableEntity entity, SerializableProperty property, string[] value, string displayName, string toolTip) + public GUIComponent CreateStringArrayField(ISerializableEntity entity, SerializableProperty property, string[] value, LocalizedString displayName, LocalizedString toolTip) { int elementCount = (value.Length + 1); var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementCount * elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -1225,8 +1224,8 @@ namespace Barotrauma var elementLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, element.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); // Set the label to be (i + 1) so it's easier to understand for non-programmers string componentLabel = (i + 1).ToString(); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), elementLayoutGroup.RectTransform) { MaxSize = new Point(25, elementLayoutGroup.Rect.Height) }, componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); - GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUI.SmallFont }; + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), elementLayoutGroup.RectTransform) { MaxSize = new Point(25, elementLayoutGroup.Rect.Height) }, componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); + GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUIStyle.SmallFont }; int comp = i; textBox.OnEnterPressed += (textBox, text) => OnApply(textBox); textBox.OnDeselected += (textBox, keys) => OnApply(textBox); @@ -1243,13 +1242,13 @@ namespace Barotrauma if (SetPropertyValue(property, entity, newValue)) { TrySendNetworkUpdate(entity, property); - textBox.Flash(color: GUI.Style.Green, flashDuration: 1f); + textBox.Flash(color: GUIStyle.Green, flashDuration: 1f); } } else { textBox.Text = newValue[comp]; - textBox.Flash(color: GUI.Style.Red, flashDuration: 1f); + textBox.Flash(color: GUIStyle.Red, flashDuration: 1f); } return true; } @@ -1268,13 +1267,13 @@ namespace Barotrauma }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Sum(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { - var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -1293,16 +1292,15 @@ namespace Barotrauma } }; - textTag = textTag.ToLowerInvariant(); - var tagTextPairs = TextManager.GetAllTagTextPairs(); + var tagTextPairs = TextManager.GetAllTagTextPairs().ToList(); tagTextPairs.Sort((t1, t2) => { return t1.Value.CompareTo(t2.Value); }); - foreach (KeyValuePair tagTextPair in tagTextPairs) + foreach (KeyValuePair tagTextPair in tagTextPairs) { if (!tagTextPair.Key.StartsWith(textTag)) { continue; } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, - ToolBox.LimitString(tagTextPair.Value, GUI.Font, textList.Content.Rect.Width)) + ToolBox.LimitString(tagTextPair.Value, GUIStyle.Font, textList.Content.Rect.Width)) { - UserData = tagTextPair.Key + UserData = tagTextPair.Key.ToString() }; } } @@ -1352,7 +1350,7 @@ namespace Barotrauma } }); - PropertyCommand cmd = new PropertyCommand(entities, property.Name, value, oldValues); + PropertyCommand cmd = new PropertyCommand(entities, property.Name.ToIdentifier(), value, oldValues); if (CommandBuffer != null) { if (CommandBuffer.Item1 == property && CommandBuffer.Item2.PropertyCount == cmd.PropertyCount) @@ -1416,8 +1414,7 @@ namespace Barotrauma else if (entity is ISerializableEntity { SerializableProperties: { } } sEntity) { var props = sEntity.SerializableProperties; - - if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) + if (props.TryGetValue(property.Name.ToIdentifier(), out SerializableProperty foundProp) && foundProp.Attributes.OfType().Any()) { SafeAdd(sEntity, foundProp); foundProp.PropertyInfo.SetValue(entity, value); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/CompletedTutorials.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/CompletedTutorials.cs new file mode 100644 index 000000000..f874026f3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/CompletedTutorials.cs @@ -0,0 +1,45 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + public class CompletedTutorials + { + private readonly HashSet identifiers = new HashSet(); + + private CompletedTutorials() { } + + private CompletedTutorials(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + identifiers.Add(subElement.GetAttributeIdentifier("name", Identifier.Empty)); + } + } + + public static void Init(XElement? element) + { + if (element is null) { return; } + + Instance = new CompletedTutorials(element); + } + + public void SaveTo(XElement element) + { + identifiers.ForEach(id => new XElement("Tutorial", new XAttribute("name", id.Value))); + } + + public bool Contains(Identifier identifier) => identifiers.Contains(identifier); + + public void Add(Identifier identifier) => identifiers.Add(identifier); + + public void Remove(Identifier identifier) => identifiers.Remove(identifier); + + public static CompletedTutorials Instance { get; private set; } = new CompletedTutorials(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/DebugConsoleMapping.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/DebugConsoleMapping.cs new file mode 100644 index 000000000..f7471c950 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/DebugConsoleMapping.cs @@ -0,0 +1,58 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma.ClientSource.Settings +{ + public class DebugConsoleMapping + { + private readonly Dictionary bindings = new Dictionary(); + public IReadOnlyDictionary Bindings => bindings; + + private DebugConsoleMapping() { } + + private DebugConsoleMapping(XElement element) + { + var bindings = new Dictionary(); + foreach (var subElement in element.Elements()) + { + KeyOrMouse keyOrMouse = subElement.GetAttributeKeyOrMouse("key", MouseButton.None); + if (keyOrMouse == MouseButton.None) { continue; } + string command = subElement.GetAttributeString("command", ""); + if (command.IsNullOrWhiteSpace()) { continue; } + bindings[keyOrMouse] = command; + } + + this.bindings = bindings; + } + + public static void Init(XElement? element) + { + if (element is null) { return; } + + Instance = new DebugConsoleMapping(element); + } + + public void SaveTo(XElement element) + { + Bindings + .ForEach(kvp => element.Add( + new XElement("Keybind", + new XAttribute("key", kvp.Key), + new XAttribute("command", kvp.Value)))); + } + + public void Set(KeyOrMouse key, string command) + => bindings[key] = command; + + public void Remove(KeyOrMouse key) + => bindings.Remove(key); + + public static DebugConsoleMapping Instance { get; private set; } = new DebugConsoleMapping(); + } + +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/IgnoredHints.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/IgnoredHints.cs new file mode 100644 index 000000000..9410a39b6 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/IgnoredHints.cs @@ -0,0 +1,42 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + public class IgnoredHints + { + private readonly HashSet identifiers = new HashSet(); + + private IgnoredHints() { } + + private IgnoredHints(XElement element) + { + identifiers = element.GetAttributeIdentifierArray("identifiers", Array.Empty()) + .ToHashSet(); + } + + public static void Init(XElement? element) + { + if (element is null) { return; } + + Instance = new IgnoredHints(element); + } + + public void SaveTo(XElement element) + { + element.SetAttributeValue("identifiers", string.Join(",", identifiers)); + } + + public bool Contains(Identifier identifier) => identifiers.Contains(identifier); + + public void Add(Identifier identifier) => identifiers.Add(identifier); + + public void Remove(Identifier identifier) => identifiers.Remove(identifier); + + public static IgnoredHints Instance { get; private set; } = new IgnoredHints(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/MultiplayerPreferences.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/MultiplayerPreferences.cs new file mode 100644 index 000000000..a1cb8dcf1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/MultiplayerPreferences.cs @@ -0,0 +1,112 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + class MultiplayerPreferences + { + public readonly struct JobPreference + { + public JobPreference(Identifier jobIdentifier, int variant) + { + JobIdentifier = jobIdentifier; + Variant = variant; + } + + public JobPreference(XElement element) : this( + element.GetAttributeIdentifier("identifier", Identifier.Empty), + element.GetAttributeInt("variant", -1)) { } + + public readonly Identifier JobIdentifier; + public readonly int Variant; + + public static bool operator ==(JobPreference a, JobPreference b) + => a.JobIdentifier == b.JobIdentifier && a.Variant == b.Variant; + + public static bool operator !=(JobPreference a, JobPreference b) => !(a == b); + + public override bool Equals(object? obj) + => obj is JobPreference jp && jp == this; + + public bool Equals(JobPreference other) => other == this; + + public override int GetHashCode() => HashCode.Combine(JobIdentifier, Variant); + } + + public readonly List JobPreferences = new List(); + public CharacterTeamType TeamPreference; + public string PlayerName = string.Empty; + + public readonly HashSet TagSet = new HashSet(); + public int HairIndex = -1; + public int BeardIndex = -1; + public int MoustacheIndex = -1; + public int FaceAttachmentIndex = -1; + public Color HairColor = Color.Black; + public Color FacialHairColor = Color.Black; + public Color SkinColor = Color.Black; + + public static MultiplayerPreferences Instance { get; private set; } = new MultiplayerPreferences(); + + private MultiplayerPreferences() { } + + private MultiplayerPreferences(IEnumerable elements) + { + foreach (var element in elements) + { + PlayerName = element.GetAttributeString("name", PlayerName); + + TagSet.UnionWith(element.GetAttributeIdentifierArray("tags", Array.Empty())); + HairIndex = element.GetAttributeInt(nameof(HairIndex), HairIndex); + BeardIndex = element.GetAttributeInt(nameof(BeardIndex), BeardIndex); + MoustacheIndex = element.GetAttributeInt(nameof(MoustacheIndex), MoustacheIndex); + FaceAttachmentIndex = element.GetAttributeInt(nameof(FaceAttachmentIndex), FaceAttachmentIndex); + + HairColor = element.GetAttributeColor(nameof(HairColor), HairColor); + FacialHairColor = element.GetAttributeColor(nameof(FacialHairColor), FacialHairColor); + SkinColor = element.GetAttributeColor(nameof(SkinColor), SkinColor); + + foreach (var subElement in element.GetChildElements("job")) + { + JobPreferences.Add(new JobPreference(subElement)); + } + } + } + + public static void Init(params XElement?[] elements) + { + Instance = new MultiplayerPreferences(elements.Where(e => e != null)!); + } + + public void SaveTo(XElement element) + { + element.SetAttributeValue("name", PlayerName); + + element.SetAttributeValue("tags", string.Join(",", TagSet)); + element.SetAttributeValue(nameof(HairIndex), HairIndex); + element.SetAttributeValue(nameof(BeardIndex), BeardIndex); + element.SetAttributeValue(nameof(MoustacheIndex), MoustacheIndex); + element.SetAttributeValue(nameof(FaceAttachmentIndex), FaceAttachmentIndex); + + element.SetAttributeValue(nameof(HairColor), HairColor.ToStringHex()); + element.SetAttributeValue(nameof(FacialHairColor), FacialHairColor.ToStringHex()); + element.SetAttributeValue(nameof(SkinColor), SkinColor.ToStringHex()); + + foreach (var jobPreference in JobPreferences) + { + element.Add(new XElement("job", + new XAttribute("identifier", jobPreference.JobIdentifier.Value), + new XAttribute("variant", jobPreference.Variant.ToString(CultureInfo.InvariantCulture)))); + } + } + + public bool AreJobPreferencesEqual(IReadOnlyList other) + => JobPreferences.SequenceEqual(other); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs new file mode 100644 index 000000000..48bde97dc --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs @@ -0,0 +1,66 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + #warning TODO: implement properly + public class ServerListFilters + { + private readonly Dictionary attributes = new Dictionary(); + + private ServerListFilters() { } + + private ServerListFilters(XElement elem) + { + if (elem == null) { return; } + foreach (var attr in elem.Attributes()) + { + attributes.Add(attr.NameAsIdentifier(), attr.Value); + } + } + + public static void Init(XElement? elem) + { + if (elem is null) { return; } + + Instance = new ServerListFilters(elem); + } + + public void SaveTo(XElement elem) + { + foreach (var kvp in attributes) + { + elem.Add(new XAttribute(kvp.Key.Value, kvp.Value)); + } + } + + public bool GetAttributeBool(Identifier key, bool def) + { + if (attributes.TryGetValue(key, out string? val)) + { + if (bool.TryParse(val, out bool result)) { return result; } + } + + return def; + } + + public T GetAttributeEnum(Identifier key, T def) where T : struct, Enum + { + if (attributes.TryGetValue(key, out string? val)) + { + if (Enum.TryParse(val, out T result)) { return result; } + } + + return def; + } + + public void SetAttribute(Identifier key, string val) + { + attributes[key] = val; + } + + public static ServerListFilters Instance { get; private set; } = new ServerListFilters(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs new file mode 100644 index 000000000..c809ad75f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -0,0 +1,752 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Barotrauma.Steam; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using OpenAL; + +namespace Barotrauma +{ + public class SettingsMenu + { + public static SettingsMenu? Instance { get; private set; } + + public enum Tab + { + Graphics, + AudioAndVC, + Controls, + Gameplay, + Mods + } + + private GameSettings.Config unsavedConfig; + + private readonly GUIFrame mainFrame; + + private readonly GUILayoutGroup tabber; + private readonly GUIFrame contentFrame; + private readonly GUILayoutGroup bottom; + + public readonly WorkshopMenu WorkshopMenu; + + public static SettingsMenu Create(RectTransform mainParent) + { + Instance?.Close(); + Instance = new SettingsMenu(mainParent); + return Instance; + } + + private SettingsMenu(RectTransform mainParent) + { + unsavedConfig = GameSettings.CurrentConfig; + + mainFrame = new GUIFrame(new RectTransform(Vector2.One, mainParent)); + + var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, mainFrame.RectTransform, Anchor.Center, Pivot.Center), + isHorizontal: false, childAnchor: Anchor.TopRight); + + new GUITextBlock(new RectTransform((1.0f, 0.07f), mainLayout.RectTransform), TextManager.Get("Settings"), + font: GUIStyle.LargeFont); + + var tabberAndContentLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.86f), mainLayout.RectTransform), + isHorizontal: true); + + void tabberPadding() + => new GUIFrame(new RectTransform((0.01f, 1.0f), tabberAndContentLayout.RectTransform), style: null); + + tabberPadding(); + tabber = new GUILayoutGroup(new RectTransform((0.06f, 1.0f), tabberAndContentLayout.RectTransform), isHorizontal: false) { AbsoluteSpacing = GUI.IntScale(5f) }; + tabberPadding(); + tabContents = new Dictionary(); + + contentFrame = new GUIFrame(new RectTransform((0.92f, 1.0f), tabberAndContentLayout.RectTransform), + style: "InnerFrame"); + + bottom = new GUILayoutGroup(new RectTransform((contentFrame.RectTransform.RelativeSize.X, 0.04f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; + + CreateGraphicsTab(); + CreateAudioAndVCTab(); + CreateControlsTab(); + CreateGameplayTab(); + CreateModsTab(out WorkshopMenu); + + CreateBottomButtons(); + + SelectTab(Tab.Graphics); + + tabber.Recalculate(); + } + + private void SwitchContent(GUIFrame newContent) + { + contentFrame.Children.ForEach(c => c.Visible = false); + newContent.Visible = true; + } + + private readonly Dictionary tabContents; + + public void SelectTab(Tab tab) + { + SwitchContent(tabContents[tab].Content); + tabber.Children.ForEach(c => + { + if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; } + }); + } + + private void AddButtonToTabber(Tab tab, GUIFrame content) + { + var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.TopLeft, Pivot.TopLeft, scaleBasis: ScaleBasis.Smallest), "", style: $"SettingsMenuTab.{tab}") + { + ToolTip = TextManager.Get($"SettingsTab.{tab}"), + OnClicked = (b, _) => + { + SelectTab(tab); + return false; + } + }; + button.RectTransform.MaxSize = RectTransform.MaxPoint; + button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + + tabContents.Add(tab, (button, content)); + } + + private GUIFrame CreateNewContentFrame(Tab tab) + { + var content = new GUIFrame(new RectTransform(Vector2.One * 0.95f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null); + AddButtonToTabber(tab, content); + return content; + } + + private static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + var centerFrame = new GUIFrame(new RectTransform((0.025f, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, centerFrame.RectTransform), + onDraw: (sb, c) => + { + sb.DrawLine((c.Rect.Center.X, c.Rect.Top),(c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); + }); + } + GUILayoutGroup right = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, right); + } + + private static GUILayoutGroup CreateCenterLayout(GUIFrame parent) + { + return new GUILayoutGroup(new RectTransform((0.5f, 1.0f), parent.RectTransform, Anchor.TopCenter, Pivot.TopCenter)) { ChildAnchor = Anchor.TopCenter }; + } + + private static RectTransform NewItemRectT(GUILayoutGroup parent) + => new RectTransform((1.0f, 0.06f), parent.RectTransform, Anchor.CenterLeft); + + private static void Spacer(GUILayoutGroup parent) + { + new GUIFrame(new RectTransform((1.0f, 0.03f), parent.RectTransform, Anchor.CenterLeft), style: null); + } + + private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font) + { + return new GUITextBlock(NewItemRectT(parent), str, font: font); + } + + private static void DropdownEnum(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, T currentValue, + Action setter) where T : Enum + => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); + + private static void Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) + { + var dropdown = new GUIDropDown(NewItemRectT(parent)); + values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); + dropdown.Select(values.IndexOf(currentValue)); + dropdown.OnSelected = (dd, obj) => + { + setter((T)obj); + return true; + }; + } + + private void Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) + { + var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); + var slider = new GUIScrollBar(new RectTransform((0.82f, 1.0f), layout.RectTransform), style: "GUISlider") + { + Range = range, + BarScrollValue = currentValue, + Step = 1.0f / (float)(steps - 1), + BarSize = 1.0f / steps + }; + if (tooltip != null) + { + slider.ToolTip = tooltip; + } + var label = new GUITextBlock(new RectTransform((0.18f, 1.0f), layout.RectTransform), + labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center); + slider.OnMoved = (sb, val) => + { + label.Text = labelFunc(sb.BarScrollValue); + setter(sb.BarScrollValue); + return true; + }; + } + + private void Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) + { + var tickbox = new GUITickBox(NewItemRectT(parent), label) + { + Selected = currentValue, + ToolTip = tooltip, + OnSelected = (tb) => + { + setter(tb.Selected); + return true; + } + }; + } + + private string ScaleResolution(float scale) => + $"{Round(unsavedConfig.Graphics.Width * scale)}\nx\n{Round(unsavedConfig.Graphics.Height * scale)}"; + + private string Percentage(float v) => $"{Round(v * 100)}%"; + + private int Round(float v) => (int)MathF.Round(v); + + private void CreateGraphicsTab() + { + GUIFrame content = CreateNewContentFrame(Tab.Graphics); + + var (left, right) = CreateSidebars(content); + + List<(int Width, int Height)> supportedResolutions = + GameMain.GraphicsDeviceManager.GraphicsDevice.Adapter.SupportedDisplayModes + .Where(m => m.Format == SurfaceFormat.Color) + .Select(m => (m.Width, m.Height)) + .ToList(); + var currentResolution = (unsavedConfig.Graphics.Width, unsavedConfig.Graphics.Height); + if (!supportedResolutions.Contains(currentResolution)) + { + supportedResolutions.Add(currentResolution); + } + + Label(left, TextManager.Get("Resolution"), GUIStyle.SubHeadingFont); + Dropdown(left, (m) => $"{m.Width}x{m.Height}", null, supportedResolutions, currentResolution, + (res) => + { + unsavedConfig.Graphics.Width = res.Width; + unsavedConfig.Graphics.Height = res.Height; + }); + Spacer(left); + + Label(left, TextManager.Get("DisplayMode"), GUIStyle.SubHeadingFont); + DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, (v) => unsavedConfig.Graphics.DisplayMode = v); + Spacer(left); + + Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, (v) => unsavedConfig.Graphics.VSync = v); + Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, (v) => unsavedConfig.Graphics.CompressTextures = v); + + Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); + Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(v)); + Spacer(right); + + Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); + DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, (v) => unsavedConfig.Graphics.LosMode = v); + Spacer(right); + + Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont); + Slider(right, (0.5f, 1.0f), 10, ScaleResolution, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); + Spacer(right); + + Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, (v) => unsavedConfig.Graphics.RadialDistortion = v); + Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, (v) => unsavedConfig.Graphics.ChromaticAberration = v); + } + + private static string TrimAudioDeviceName(string name) + { + if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } + string[] prefixes = { "OpenAL Soft on " }; + foreach (string prefix in prefixes) + { + if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return name.Remove(0, prefix.Length); + } + } + return name; + } + + private static int HandleAlErrors(string message) + { + int alcError = Alc.GetError(IntPtr.Zero); + if (alcError != Alc.NoError) + { + DebugConsole.ThrowError($"{message}: ALC error {Alc.GetErrorString(alcError)}"); + return alcError; + } + + int alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError($"{message}: AL error {Al.GetErrorString(alError)}"); + return alError; + } + + return Al.NoError; + } + + private static void GetAudioDevices(int listSpecifier, int defaultSpecifier, out IReadOnlyList list, ref string current) + { + list = Array.Empty(); + + var retVal = Alc.GetStringList(IntPtr.Zero, listSpecifier).ToList(); + if (HandleAlErrors("Alc.GetStringList failed") != Al.NoError) { return; } + + list = retVal; + if (string.IsNullOrEmpty(current)) + { + current = Alc.GetString(IntPtr.Zero, defaultSpecifier); + if (HandleAlErrors("Alc.GetString failed") != Al.NoError) { return; } + } + + string currentVal = current; + if (list.Any() && !list.Any(n => n.Equals(currentVal, StringComparison.OrdinalIgnoreCase))) + { + current = list[0]; + } + } + + private void CreateAudioAndVCTab() + { + if (GameMain.Client == null + && VoipCapture.Instance == null) + { + string currDevice = unsavedConfig.Audio.VoiceCaptureDevice; + GetAudioDevices(Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, out var deviceList, ref currDevice); + + if (deviceList.Any()) + { + VoipCapture.Create(unsavedConfig.Audio.VoiceCaptureDevice); + } + if (VoipCapture.Instance == null) + { + unsavedConfig.Audio.VoiceSetting = VoiceMode.Disabled; + } + } + + GUIFrame content = CreateNewContentFrame(Tab.AudioAndVC); + + var (audio, voiceChat) = CreateSidebars(content, split: true); + + static void audioDeviceElement( + GUILayoutGroup parent, + Action setter, + int listSpecifier, + int defaultSpecifier, + ref string currentDevice) + { +#if OSX + //At the time of writing there are no OpenAL implementations + //on macOS that return the list of available devices, or + //allow selecting any other than the default one. I'm not + //about to write my own OpenAL implementation to fix this + //so here's a workaround instead, just a label that shows the + //name of the current device. + var deviceNameContainerElement = new GUIFrame(NewItemRectT(parent), style: "GUITextBoxNoIcon"); + var deviceNameElement = new GUITextBlock(new RectTransform(Vector2.One, deviceNameContainerElement.RectTransform), currentDevice, textAlignment: Alignment.CenterLeft); + new GUICustomComponent(new RectTransform(Vector2.Zero, deviceNameElement.RectTransform), onUpdate: + (deltaTime, component) => + { + deviceNameElement.Text = Alc.GetString(IntPtr.Zero, listSpecifier); + }); +#else + GetAudioDevices(listSpecifier, defaultSpecifier, out var devices, ref currentDevice); + Dropdown(parent, v => TrimAudioDeviceName(v), null, devices, currentDevice, setter); +#endif + } + + Label(audio, TextManager.Get("AudioOutputDevice"), GUIStyle.SubHeadingFont); + + string currentOutputDevice = unsavedConfig.Audio.AudioOutputDevice; + audioDeviceElement(audio, v => unsavedConfig.Audio.AudioOutputDevice = v, Alc.OutputDevicesSpecifier, Alc.DefaultDeviceSpecifier, ref currentOutputDevice); + Spacer(audio); + + Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, (v) => unsavedConfig.Audio.SoundVolume = v); + + Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, (v) => unsavedConfig.Audio.MusicVolume = v); + + Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, (v) => unsavedConfig.Audio.MuteOnFocusLost = v); + Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, (v) => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); + Spacer(audio); + + Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, (v) => unsavedConfig.Audio.VoiceChatVolume = v); + + Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, (v) => unsavedConfig.Audio.UseDirectionalVoiceChat = v); + Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, (v) => unsavedConfig.Audio.VoipAttenuationEnabled = v); + + Label(voiceChat, TextManager.Get("AudioInputDevice"), GUIStyle.SubHeadingFont); + + string currentInputDevice = unsavedConfig.Audio.VoiceCaptureDevice; + audioDeviceElement(voiceChat, v => unsavedConfig.Audio.VoiceCaptureDevice = v, Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, ref currentInputDevice); + Spacer(voiceChat); + + Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont); + DropdownEnum(voiceChat, (v) => TextManager.Get($"VoiceMode.{v}"), (v) => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, (v) => unsavedConfig.Audio.VoiceSetting = v); + Spacer(voiceChat); + + var noiseGateThresholdLabel = Label(voiceChat, TextManager.Get("NoiseGateThreshold"), GUIStyle.SubHeadingFont); + var dbMeter = new GUIProgressBar(NewItemRectT(voiceChat), 0.0f, Color.Lime); + dbMeter.ProgressGetter = () => + { + if (VoipCapture.Instance == null) { return 0.0f; } + + dbMeter.Color = unsavedConfig.Audio.VoiceSetting switch + { + VoiceMode.Activity => VoipCapture.Instance.LastdB > unsavedConfig.Audio.NoiseGateThreshold ? GUIStyle.Green : GUIStyle.Orange, + VoiceMode.PushToTalk => GUIStyle.Green, + VoiceMode.Disabled => Color.LightGray + }; + + float scrollVal = double.IsNegativeInfinity(VoipCapture.Instance.LastdB) ? 0.0f : ((float)VoipCapture.Instance.LastdB + 100.0f) / 100.0f; + return scrollVal * scrollVal; + }; + var noiseGateSlider = new GUIScrollBar(new RectTransform(Vector2.One, dbMeter.RectTransform, Anchor.Center), color: Color.White, + style: "GUISlider", barSize: 0.03f); + noiseGateSlider.Frame.Visible = false; + noiseGateSlider.Step = 0.01f; + noiseGateSlider.Range = new Vector2(-100.0f, 0.0f); + noiseGateSlider.BarScroll = MathUtils.InverseLerp(-100.0f, 0.0f, unsavedConfig.Audio.NoiseGateThreshold); + noiseGateSlider.BarScroll *= noiseGateSlider.BarScroll; + noiseGateSlider.OnMoved = (scrollBar, barScroll) => + { + unsavedConfig.Audio.NoiseGateThreshold = MathHelper.Lerp(-100.0f, 0.0f, (float)Math.Sqrt(scrollBar.BarScroll)); + return true; + }; + new GUICustomComponent(new RectTransform(Vector2.Zero, voiceChat.RectTransform), onUpdate: + (deltaTime, component) => + { + noiseGateThresholdLabel.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity; + noiseGateSlider.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity; + }); + Spacer(voiceChat); + + Label(voiceChat, TextManager.Get("MicrophoneVolume"), GUIStyle.SubHeadingFont); + Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, (v) => unsavedConfig.Audio.MicrophoneVolume = v); + Spacer(voiceChat); + + Label(voiceChat, TextManager.Get("CutoffPrevention"), GUIStyle.SubHeadingFont); + Slider(voiceChat, (0, 500), 26, (v) => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, (v) => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); + } + + private void CreateControlsTab() + { + GUIFrame content = CreateNewContentFrame(Tab.Controls); + + GUILayoutGroup layout = CreateCenterLayout(content); + + Label(layout, TextManager.Get("AimAssist"), GUIStyle.SubHeadingFont); + Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, (v) => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); + Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, (v) => unsavedConfig.EnableMouseLook = v); + Spacer(layout); + + GUIListBox keyMapList = + new GUIListBox(new RectTransform((2.0f, 0.7f), + layout.RectTransform)) + { + CanBeFocused = false, + OnSelected = (_, __) => false + }; + Spacer(layout); + + GUILayoutGroup createInputRowLayout() + => new GUILayoutGroup(new RectTransform((1.0f, 0.1f), keyMapList.Content.RectTransform), isHorizontal: true); + + HashSet inputButtons = new HashSet(); + Action? currentSetter = null; + void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func valueNameGetter, Action valueSetter) + { + var inputFrame = new GUIFrame(new RectTransform((0.5f, 1.0f), currRow.RectTransform), + style: null); + var label = new GUITextBlock(new RectTransform((0.6f, 1.0f), inputFrame.RectTransform), labelText, + font: GUIStyle.SmallFont) {ForceUpperCase = ForceUpperCase.Yes}; + var inputBox = new GUIButton( + new RectTransform((0.4f, 1.0f), inputFrame.RectTransform, Anchor.TopRight, Pivot.TopRight), + valueNameGetter(), style: "GUITextBoxNoIcon") + { + OnClicked = (btn, obj) => + { + inputButtons.ForEach(b => + { + if (b != btn) { b.Selected = false; } + }); + bool willBeSelected = !btn.Selected; + if (willBeSelected) + { + currentSetter = (v) => + { + valueSetter(v); + btn.Text = valueNameGetter(); + }; + } + else + { + currentSetter = null; + } + + btn.Selected = willBeSelected; + return true; + } + }; + inputButtons.Add(inputBox); + } + + var inputListener = new GUICustomComponent(new RectTransform(Vector2.Zero, layout.RectTransform), onUpdate: (deltaTime, component) => + { + if (currentSetter is null) { return; } + + void clearSetter() + { + currentSetter = null; + inputButtons.ForEach(b => b.Selected = false); + } + + void callSetter(KeyOrMouse v) + { + currentSetter?.Invoke(v); + clearSetter(); + } + + var pressedKeys = PlayerInput.GetKeyboardState.GetPressedKeys(); + if ((pressedKeys?.Any() ?? false)) + { + if (pressedKeys.Contains(Keys.Escape)) + { + clearSetter(); + } + else + { + callSetter(pressedKeys.First()); + } + } + else if (PlayerInput.PrimaryMouseButtonClicked() && !(GUI.MouseOn is GUIButton)) + { + callSetter(MouseButton.PrimaryMouse); + } + else if (PlayerInput.SecondaryMouseButtonClicked()) + { + callSetter(MouseButton.SecondaryMouse); + } + else if (PlayerInput.MidButtonClicked()) + { + callSetter(MouseButton.MiddleMouse); + } + else if (PlayerInput.Mouse4ButtonClicked()) + { + callSetter(MouseButton.MouseButton4); + } + else if (PlayerInput.Mouse5ButtonClicked()) + { + callSetter(MouseButton.MouseButton5); + } + else if (PlayerInput.MouseWheelUpClicked()) + { + callSetter(MouseButton.MouseWheelUp); + } + else if (PlayerInput.MouseWheelDownClicked()) + { + callSetter(MouseButton.MouseWheelDown); + } + }); + + InputType[] inputTypes = (InputType[])Enum.GetValues(typeof(InputType)); + InputType[][] inputTypeColumns = + { + inputTypes.Take(inputTypes.Length - (inputTypes.Length / 2)).ToArray(), + inputTypes.TakeLast(inputTypes.Length / 2).ToArray() + }; + for (int i = 0; i < inputTypes.Length; i+=2) + { + var currRow = createInputRowLayout(); + for (int j = 0; j < 2; j++) + { + var column = inputTypeColumns[j]; + if (i / 2 >= column.Length) { break; } + var input = column[i / 2]; + addInputToRow( + currRow, + TextManager.Get($"InputType.{input}"), + () => unsavedConfig.KeyMap.Bindings[input].Name, + (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v)); + } + } + + for (int i = 0; i < unsavedConfig.InventoryKeyMap.Bindings.Length; i += 2) + { + var currRow = createInputRowLayout(); + for (int j = 0; j < 2; j++) + { + int currIndex = i + j; + if (currIndex >= unsavedConfig.InventoryKeyMap.Bindings.Length) { break; } + + var input = unsavedConfig.InventoryKeyMap.Bindings[currIndex]; + addInputToRow( + currRow, + TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex+1).ToString(CultureInfo.InvariantCulture)), + () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name, + (v) => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); + } + } + + GUILayoutGroup resetControlsHolder = + new GUILayoutGroup(new RectTransform((1.75f, 0.1f), layout.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.1f + }; + + var defaultBindingsButton = + new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), + TextManager.Get("SetDefaultBindings"), style: "GUIButtonSmall") + { + ToolTip = TextManager.Get("SetDefaultBindingsTooltip") + }; + + var legacyBindingsButton = + new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), + TextManager.Get("SetLegacyBindings"), style: "GUIButtonSmall") + { + ToolTip = TextManager.Get("SetLegacyBindingsTooltip") + }; + } + + private void CreateGameplayTab() + { + GUIFrame content = CreateNewContentFrame(Tab.Gameplay); + + GUILayoutGroup layout = CreateCenterLayout(content); + + var languages = TextManager.AvailableLanguages + .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) + .ToArray(); + Label(layout, TextManager.Get("Language"), GUIStyle.SubHeadingFont); + Dropdown(layout, (v) => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, (v) => unsavedConfig.Language = v); + Spacer(layout); + + Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, (v) => unsavedConfig.PauseOnFocusLost = v); + Spacer(layout); + + Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, (v) => unsavedConfig.DisableInGameHints = v); + var resetInGameHintsButton = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), + TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") + { + ToolTip = TextManager.Get("ResetInGameHintsTooltip") + }; + Spacer(layout); + + Label(layout, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, (v) => unsavedConfig.Graphics.HUDScale = v); + Label(layout, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, (v) => unsavedConfig.Graphics.InventoryScale = v); + Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, (v) => unsavedConfig.Graphics.TextScale = v); + +#if !OSX + Spacer(layout); + var statisticsTickBox = new GUITickBox(NewItemRectT(layout), TextManager.Get("statisticsconsenttickbox")) + { + OnSelected = tickBox => + { + GameAnalyticsManager.SetConsent( + tickBox.Selected + ? GameAnalyticsManager.Consent.Ask + : GameAnalyticsManager.Consent.No); + return false; + } + }; +#if DEBUG + statisticsTickBox.Enabled = false; +#endif + void updateGATickBoxToolTip() + => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); + updateGATickBoxToolTip(); + + var cachedConsent = GameAnalyticsManager.Consent.Unknown; + var statisticsTickBoxUpdater = new GUICustomComponent( + new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform), + onUpdate: (deltaTime, component) => + { + bool shouldTickBoxBeSelected = GameAnalyticsManager.UserConsented == GameAnalyticsManager.Consent.Yes; + + bool shouldUpdateTickBoxState = cachedConsent != GameAnalyticsManager.UserConsented + || statisticsTickBox.Selected != shouldTickBoxBeSelected; + + if (!shouldUpdateTickBoxState) { return; } + + updateGATickBoxToolTip(); + cachedConsent = GameAnalyticsManager.UserConsented; + GUITickBox.OnSelectedHandler prevHandler = statisticsTickBox.OnSelected; + statisticsTickBox.OnSelected = null; + statisticsTickBox.Selected = shouldTickBoxBeSelected; + statisticsTickBox.OnSelected = prevHandler; + statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; + }); +#endif + } + + private void CreateModsTab(out WorkshopMenu workshopMenu) + { + GUIFrame content = CreateNewContentFrame(Tab.Mods); + content.RectTransform.RelativeSize = Vector2.One; + + workshopMenu = new WorkshopMenu(content); + } + + private void CreateBottomButtons() + { + GUIButton cancelButton = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: "Cancel") + { + OnClicked = (btn, obj) => + { + Close(); + return false; + } + }; + GUIButton applyButton = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: "Apply") + { + OnClicked = (btn, obj) => + { + GameSettings.SetCurrentConfig(unsavedConfig); + WorkshopMenu.Apply(); + GameSettings.SaveCurrentConfig(); + mainFrame.Flash(color: GUIStyle.Green); + return false; + } + }; + } + + public void Close() + { + if (GameMain.Client is null || GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) + { + VoipCapture.Instance?.Dispose(); + } + mainFrame.Parent.RemoveChild(mainFrame); + if (Instance == this) { Instance = null; } + + GUI.SettingsMenuOpen = false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs index 785c510b4..96df43626 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs @@ -109,6 +109,14 @@ namespace OpenAL public const int CaptureDefaultDeviceSpecifier = 0x311; public const int EnumCaptureSamples = 0x312; public const int EnumConnected = 0x313; + + + public const int OutputDevicesSpecifier = +#if OSX + DeviceSpecifier; +#else + AllDevicesSpecifier; +#endif #endregion @@ -214,7 +222,7 @@ namespace OpenAL return Encoding.UTF8.GetString(bytes); } - public static IList GetStringList(IntPtr device, int param) + public static IReadOnlyList GetStringList(IntPtr device, int param) { List retVal = new List(); IntPtr strPtr = _GetString(device, param); @@ -224,7 +232,8 @@ namespace OpenAL byte currChar = Marshal.ReadByte(strPtr, strEnd); if (currChar == '\0') { return retVal; } byte prevChar = 255; - while (true) { + while (true) + { strEnd++; prevChar = currChar; currChar = Marshal.ReadByte(strPtr, strEnd); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 281b031f2..41675864d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -234,7 +234,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - DebugConsole.ThrowError("Failed to set source's gain: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + DebugConsole.ThrowError($"Failed to set source's gain to {gain} (effective gain {effectiveGain}): {debugName}, {Al.GetErrorString(alError)}", appendStackTrace: true); return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index b967865ac..ac40b98bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -207,7 +207,7 @@ namespace Barotrauma.Sounds playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; playingChannels[(int)SourcePoolIndex.Voice] = new SoundChannel[16]; - string deviceName = GameMain.Config.AudioOutputDevice; + string deviceName = GameSettings.CurrentConfig.Audio.AudioOutputDevice; if (string.IsNullOrEmpty(deviceName)) { @@ -221,7 +221,10 @@ namespace Barotrauma.Sounds deviceName = audioDeviceNames[0]; } #endif - GameMain.Config.AudioOutputDevice = deviceName; + if (GameSettings.CurrentConfig.Audio.AudioOutputDevice != deviceName) + { + SetAudioOutputDevice(deviceName); + } InitializeAlcDevice(deviceName); @@ -232,6 +235,13 @@ namespace Barotrauma.Sounds CompressionDynamicRangeGain = 1.0f; } + private void SetAudioOutputDevice(string deviceName) + { + var config = GameSettings.CurrentConfig; + config.Audio.AudioOutputDevice = deviceName; + GameSettings.SetCurrentConfig(config); + } + public bool InitializeAlcDevice(string deviceName) { ReleaseResources(true); @@ -351,11 +361,11 @@ namespace Barotrauma.Sounds return newSound; } - public Sound LoadSound(XElement element, bool stream = false, string overrideFilePath = null) + public Sound LoadSound(ContentXElement element, bool stream = false, string overrideFilePath = null) { if (Disabled) { return null; } - string filePath = overrideFilePath ?? element.GetAttributeString("file", ""); + string filePath = overrideFilePath ?? element.GetAttributeContentPath("file")?.Value ?? ""; if (!File.Exists(filePath)) { throw new System.IO.FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); @@ -631,13 +641,13 @@ namespace Barotrauma.Sounds if (isConnected == 0) { DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); - GameMain.Config.AudioOutputDevice = ""; + SetAudioOutputDevice(""); Disconnected = true; return; } } - if (GameMain.Client != null && GameMain.Config.VoipAttenuationEnabled) + if (GameMain.Client != null && GameSettings.CurrentConfig.Audio.VoipAttenuationEnabled) { if (Timing.TotalTime > lastAttenuationTime+0.2) { @@ -653,7 +663,7 @@ namespace Barotrauma.Sounds SetCategoryGainMultiplier("waterambience", VoipAttenuatedGain, 1); SetCategoryGainMultiplier("music", VoipAttenuatedGain, 1); - if (GameMain.Config.DynamicRangeCompressionEnabled) + if (GameSettings.CurrentConfig.Audio.DynamicRangeCompressionEnabled) { float targetGain = (Math.Min(1.0f, 1.0f / PlaybackAmplitude) - 1.0f) * 0.5f + 1.0f; if (targetGain < CompressionDynamicRangeGain) @@ -695,6 +705,15 @@ namespace Barotrauma.Sounds } } + public void ApplySettings() + { + SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier("ui", GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier("music", GameSettings.CurrentConfig.Audio.MusicVolume, 0); + SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); + } + public void InitStreamThread() { if (Disabled) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 57920dea6..177cb11a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -9,78 +9,28 @@ using System.Xml.Linq; namespace Barotrauma { - public struct DamageSound - { - //the range of inflicted damage where the sound can be played - //(10.0f, 30.0f) would be played when the inflicted damage is between 10 and 30 - public readonly Vector2 damageRange; - - public readonly string damageType; - - public readonly Sound sound; - - public readonly string requiredTag; - - public bool ignoreMuffling; - - public DamageSound(Sound sound, Vector2 damageRange, string damageType, bool ignoreMuffling, string requiredTag = "") - { - this.sound = sound; - this.damageRange = damageRange; - this.damageType = damageType; - this.ignoreMuffling = ignoreMuffling; - this.requiredTag = requiredTag; - } - } - - public class BackgroundMusic - { - public readonly string File; - public readonly string Type; - public readonly bool DuckVolume; - public readonly float Volume; - - public readonly Vector2 IntensityRange; - - public readonly bool ContinueFromPreviousTime; - public int PreviousTime; - - public readonly XElement Element; - - public BackgroundMusic(XElement element) - { - this.File = Path.GetFullPath(element.GetAttributeString("file", "")).CleanUpPath(); - this.Type = element.GetAttributeString("type", "").ToLowerInvariant(); - this.IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); - this.DuckVolume = element.GetAttributeBool("duckvolume", false); - this.Volume = element.GetAttributeFloat("volume", 1.0f); - this.ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); - this.Element = element; - } - } - static class SoundPlayer { - private static ILookup miscSounds; - //music private const float MusicLerpSpeed = 1.0f; private const float UpdateMusicInterval = 5.0f; const int MaxMusicChannels = 6; - private readonly static Sound[] currentMusic = new Sound[MaxMusicChannels]; + private readonly static BackgroundMusic[] currentMusic = new BackgroundMusic[MaxMusicChannels]; private readonly static SoundChannel[] musicChannel = new SoundChannel[MaxMusicChannels]; private readonly static BackgroundMusic[] targetMusic = new BackgroundMusic[MaxMusicChannels]; - private static List musicClips; + private static IEnumerable musicClips => BackgroundMusic.BackgroundMusicPrefabs; private static BackgroundMusic previousDefaultMusic; private static float updateMusicTimer; //ambience - private static Sound waterAmbienceIn, waterAmbienceOut, waterAmbienceMoving; - private static readonly SoundChannel[] waterAmbienceChannels = new SoundChannel[3]; + private static Sound waterAmbienceIn => SoundPrefab.WaterAmbienceIn.ActivePrefab.Sound; + private static Sound waterAmbienceOut => SoundPrefab.WaterAmbienceOut.ActivePrefab.Sound; + private static Sound waterAmbienceMoving => SoundPrefab.WaterAmbienceMoving.ActivePrefab.Sound; + private static readonly HashSet waterAmbienceChannels = new HashSet(); private static float ambientSoundTimer; private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max @@ -92,8 +42,8 @@ namespace Barotrauma //misc private static float[] targetFlowLeft, targetFlowRight; - public static List FlowSounds = new List(); - public static List SplashSounds = new List(); + public static IReadOnlyList FlowSounds => SoundPrefab.FlowSounds; + public static IReadOnlyList SplashSounds => SoundPrefab.SplashSounds; private static SoundChannel[] flowSoundChannels; private static float[] flowVolumeLeft; private static float[] flowVolumeRight; @@ -112,17 +62,13 @@ namespace Barotrauma private static string[] fireSoundTags = new string[fireSizes] { "fire", "firemedium", "firelarge" }; // TODO: could use a dictionary to split up the list into smaller lists of same type? - private static List damageSounds; - - private static Dictionary> guiSounds; + private static IEnumerable damageSounds => DamageSound.DamageSoundPrefabs; private static bool firstTimeInMainMenu = true; - private static Sound startUpSound; + private static Sound startUpSound => SoundPrefab.StartupSound.ActivePrefab.Sound; - public static bool Initialized; - - public static string OverrideMusicType + public static Identifier OverrideMusicType { get; set; @@ -130,291 +76,35 @@ namespace Barotrauma public static float? OverrideMusicDuration; - public static int SoundCount; - - private static List loadedSoundElements; - - private static bool SoundElementsEquivalent(XElement a, XElement b) - { - string filePathA = a.GetAttributeString("file", "").CleanUpPath(); - float baseGainA = a.GetAttributeFloat("volume", 1.0f); - float rangeA = a.GetAttributeFloat("range", 1000.0f); - string filePathB = b.GetAttributeString("file", "").CleanUpPath(); - float baseGainB = b.GetAttributeFloat("volume", 1.0f); - float rangeB = b.GetAttributeFloat("range", 1000.0f); - return a.Name.ToString().Equals(b.Name.ToString(), StringComparison.OrdinalIgnoreCase) && - filePathA == filePathB && MathUtils.NearlyEqual(baseGainA, baseGainB) && - MathUtils.NearlyEqual(rangeA, rangeB); - } - - public static IEnumerable Init() - { - OverrideMusicType = null; - - var soundFiles = GameMain.Instance.GetFilesOfType(ContentType.Sounds).ToList(); - - List soundElements = new List(); - foreach (ContentFile soundFile in soundFiles) - { - XDocument doc = XMLExtensions.TryLoadXml(soundFile.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - DebugConsole.NewMessage($"Overriding all sounds with {soundFile.Path}", Color.Yellow); - soundElements.Clear(); - } - soundElements.AddRange(mainElement.Elements()); - } - - SoundCount = 1 + soundElements.Count(); - - var startUpSoundElement = soundElements.Find(e => e.Name.ToString().Equals("startupsound", StringComparison.OrdinalIgnoreCase)); - if (startUpSoundElement != null) - { - startUpSound = GameMain.SoundManager.LoadSound(startUpSoundElement, false); - startUpSound?.Play(); - } - - yield return CoroutineStatus.Running; - - List> miscSoundList = new List>(); - damageSounds ??= new List(); - musicClips ??= new List(); - guiSounds ??= new Dictionary>(); - - bool firstWaterAmbienceLoaded = false; - - foreach (XElement soundElement in soundElements) - { - yield return CoroutineStatus.Running; - - if (loadedSoundElements != null && loadedSoundElements.Any(e => SoundElementsEquivalent(e, soundElement))) - { - continue; - } - - try - { - switch (soundElement.Name.ToString().ToLowerInvariant()) - { - case "music": - var newMusicClip = new BackgroundMusic(soundElement); - if (File.Exists(newMusicClip.File)) - { - musicClips.AddIfNotNull(newMusicClip); - if (loadedSoundElements != null) - { - if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) - { - targetMusic[0] = newMusicClip; - } - } - } - else - { - DebugConsole.NewMessage($"Music file \"{newMusicClip.File}\" not found."); - } - break; - case "splash": - SplashSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); - break; - case "flow": - FlowSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); - break; - case "waterambience": - //backwards compatibility (1st waterambience used to be played both inside and outside, 2nd when moving) - if (!firstWaterAmbienceLoaded) - { - waterAmbienceIn?.Dispose(); - waterAmbienceOut?.Dispose(); - if (File.Exists(soundElement.GetAttributeString("file", ""))) - { - waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); - waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); - } - else - { - waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceIn.ogg"); - waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceOut.ogg"); - } - firstWaterAmbienceLoaded = true; - } - else - { - waterAmbienceMoving?.Dispose(); - if (File.Exists(soundElement.GetAttributeString("file", ""))) - { - waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); - } - else - { - waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceMoving.ogg"); - } - } - break; - case "waterambiencein": - waterAmbienceIn?.Dispose(); - waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); - break; - case "waterambienceout": - waterAmbienceOut?.Dispose(); - waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); - break; - case "waterambiencemoving": - waterAmbienceMoving?.Dispose(); - waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); - break; - case "damagesound": - Sound damageSound = GameMain.SoundManager.LoadSound(soundElement, false); - if (damageSound == null) { continue; } - - string damageSoundType = soundElement.GetAttributeString("damagesoundtype", "None"); - damageSounds.Add(new DamageSound( - damageSound, - soundElement.GetAttributeVector2("damagerange", Vector2.Zero), - damageSoundType, - soundElement.GetAttributeBool("ignoremuffling", false), - soundElement.GetAttributeString("requiredtag", ""))); - - break; - case "guisound": - Sound guiSound = GameMain.SoundManager.LoadSound(soundElement, stream: false); - if (guiSound == null) { continue; } - if (Enum.TryParse(soundElement.GetAttributeString("guisoundtype", null), true, out GUISoundType soundType)) - { - if (guiSounds.ContainsKey(soundType)) - { - guiSounds[soundType].Add(guiSound); - } - else - { - guiSounds.Add(soundType, new List() { guiSound }); - } - } - break; - default: - Sound sound = GameMain.SoundManager.LoadSound(soundElement, false); - if (sound != null) - { - miscSoundList.Add(new KeyValuePair(soundElement.Name.ToString().ToLowerInvariant(), sound)); - } - break; - } - } - catch (System.IO.FileNotFoundException e) - { - DebugConsole.ThrowError("Error while initializing SoundPlayer.", e); - } - } - - musicClips.RemoveAll(mc => !soundElements.Any(e => SoundElementsEquivalent(mc.Element, e))); - - for (int i = 0; i < currentMusic.Length; i++) - { - if (currentMusic[i] != null && !musicClips.Any(mc => mc.File == currentMusic[i].Filename)) - { - DisposeMusicChannel(i); - } - } - - SplashSounds.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - SplashSounds.RemoveAll(s => s.Disposed); - - FlowSounds.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - FlowSounds.RemoveAll(s => s.Disposed); - - damageSounds.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.sound.XElement, e))) { s.sound.Dispose(); } - }); - damageSounds.RemoveAll(s => s.sound.Disposed); - - guiSounds.ForEach(kvp => - { - kvp.Value?.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - }); - guiSounds.ForEach(kvp => kvp.Value?.RemoveAll(s => s.Disposed)); - - miscSounds?.ForEach(g => g.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - else { miscSoundList.Add(new KeyValuePair(g.Key, s)); } - })); - - flowSoundChannels?.ForEach(ch => ch?.Dispose()); - flowSoundChannels = new SoundChannel[FlowSounds.Count]; - flowVolumeLeft = new float[FlowSounds.Count]; - flowVolumeRight = new float[FlowSounds.Count]; - targetFlowLeft = new float[FlowSounds.Count]; - targetFlowRight = new float[FlowSounds.Count]; - - fireSoundChannels?.ForEach(ch => ch?.Dispose()); - fireSoundChannels = new SoundChannel[fireSizes]; - fireVolumeLeft = new float[fireSizes]; - fireVolumeRight = new float[fireSizes]; - - miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); - - Initialized = true; - - loadedSoundElements = soundElements; - - yield return CoroutineStatus.Success; - - } - public static void Update(float deltaTime) { - if (!Initialized) { return; } - UpdateMusic(deltaTime); - - if (startUpSound != null && !GameMain.SoundManager.IsPlaying(startUpSound)) + if (flowSoundChannels == null || flowSoundChannels.Length != FlowSounds.Count) { - startUpSound.Dispose(); - startUpSound = null; + flowSoundChannels = new SoundChannel[FlowSounds.Count]; + flowVolumeLeft = new float[FlowSounds.Count]; + flowVolumeRight = new float[FlowSounds.Count]; + targetFlowLeft = new float[FlowSounds.Count]; + targetFlowRight = new float[FlowSounds.Count]; } - + if (fireSoundChannels == null || fireSoundChannels.Length != fireSizes) + { + fireSoundChannels = new SoundChannel[fireSizes]; + fireVolumeLeft = new float[fireSizes]; + fireVolumeRight = new float[fireSizes]; + } + //stop water sounds if no sub is loaded if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen) { - for (int i = 0; i < waterAmbienceChannels.Length; i++) + foreach (var chn in waterAmbienceChannels.Concat(flowSoundChannels).Concat(fireSoundChannels)) { - if (waterAmbienceChannels[i] == null) { continue; } - waterAmbienceChannels[i].FadeOutAndDispose(); - waterAmbienceChannels[i] = null; - } - for (int i = 0; i < FlowSounds.Count; i++) - { - if (flowSoundChannels[i] == null) { continue; } - flowSoundChannels[i].FadeOutAndDispose(); - flowSoundChannels[i] = null; - } - for (int i = 0; i < fireSoundChannels.Length; i++) - { - if (fireSoundChannels[i] == null) { continue; } - fireSoundChannels[i].FadeOutAndDispose(); - fireSoundChannels[i] = null; + chn?.FadeOutAndDispose(); } fireVolumeLeft[0] = 0.0f; fireVolumeLeft[1] = 0.0f; fireVolumeRight[0] = 0.0f; fireVolumeRight[1] = 0.0f; - if (hullSoundChannel != null) - { - hullSoundChannel.FadeOutAndDispose(); - hullSoundChannel = null; - hullSoundSource = null; - } + hullSoundChannel?.FadeOutAndDispose(); + hullSoundSource = null; return; } @@ -482,44 +172,29 @@ namespace Barotrauma } } - for (int i = 0; i < 3; i++) + void updateWaterAmbience(Sound sound, float volume) { - float volume = 0.0f; - Sound sound = null; - switch (i) + SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound); + if (chn is null || !chn.IsPlaying) { - case 0: - volume = ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor; - sound = waterAmbienceIn; - break; - case 1: - volume = ambienceVolume * movementSoundVolume * insideSubFactor; - sound = waterAmbienceMoving; - break; - case 2: - volume = 1.0f - insideSubFactor; - sound = waterAmbienceOut; - break; + if (!(chn is null)) { waterAmbienceChannels.Remove(chn); } + chn = sound.Play(volume, "waterambience"); + chn.Looping = true; + waterAmbienceChannels.Add(chn); } - - if (sound == null) { continue; } - - // Consider the volume set in sounds.xml - volume *= sound.BaseGain; - if ((waterAmbienceChannels[i] == null || !waterAmbienceChannels[i].IsPlaying) && volume > 0.01f) + else { - waterAmbienceChannels[i] = sound.Play(volume, "waterambience"); - waterAmbienceChannels[i].Looping = true; - } - else if (waterAmbienceChannels[i] != null) - { - waterAmbienceChannels[i].Gain += deltaTime * Math.Sign(volume - waterAmbienceChannels[i].Gain); - if (waterAmbienceChannels[i].Gain < 0.01f) + chn.Gain += deltaTime * Math.Sign(volume - chn.Gain); + if (chn.Gain < 0.01f) { - waterAmbienceChannels[i].FadeOutAndDispose(); + chn.FadeOutAndDispose(); } } } + + updateWaterAmbience(waterAmbienceIn, ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor); + updateWaterAmbience(waterAmbienceMoving, ambienceVolume * movementSoundVolume * insideSubFactor); + updateWaterAmbience(waterAmbienceOut, 1.0f - insideSubFactor); } private static void UpdateWaterFlowSounds(float deltaTime) @@ -606,7 +281,7 @@ namespace Barotrauma Vector2 soundPos = new Vector2(GameMain.SoundManager.ListenerPosition.X + (flowVolumeRight[i] - flowVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y); if (flowSoundChannels[i] == null || !flowSoundChannels[i].IsPlaying) { - flowSoundChannels[i] = FlowSounds[i].Play(1.0f, FlowSoundRange, soundPos); + flowSoundChannels[i] = FlowSounds[i].Sound.Play(1.0f, FlowSoundRange, soundPos); flowSoundChannels[i].Looping = true; } flowSoundChannels[i].Gain = Math.Min(Math.Max(flowVolumeRight[i], flowVolumeLeft[i]), 1.0f); @@ -624,7 +299,7 @@ namespace Barotrauma } Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { foreach (FireSource fs in hull.FireSources) { @@ -760,10 +435,10 @@ namespace Barotrauma public static Sound GetSound(string soundTag) { - var matchingSounds = miscSounds[soundTag].ToList(); - if (matchingSounds.Count == 0) return null; + var matchingSounds = SoundPrefab.Prefabs.Where(p => p.ElementName == soundTag); + if (!matchingSounds.Any()) return null; - return matchingSounds[Rand.Int(matchingSounds.Count)]; + return matchingSounds.GetRandomUnsynced().Sound; } /// @@ -806,14 +481,14 @@ namespace Barotrauma private static void UpdateMusic(float deltaTime) { - if (musicClips == null || GameMain.SoundManager.Disabled) { return; } + if (musicClips == null || (GameMain.SoundManager?.Disabled ?? true)) { return; } if (OverrideMusicType != null && OverrideMusicDuration.HasValue) { OverrideMusicDuration -= deltaTime; if (OverrideMusicDuration <= 0.0f) { - OverrideMusicType = null; + OverrideMusicType = Identifier.Empty; OverrideMusicDuration = null; } } @@ -824,7 +499,7 @@ namespace Barotrauma if (updateMusicTimer <= 0.0f) { //find appropriate music for the current situation - string currentMusicType = GetCurrentMusicType(); + Identifier currentMusicType = GetCurrentMusicType(); float currentIntensity = GameMain.GameSession?.EventManager != null ? GameMain.GameSession.EventManager.MusicIntensity * 100.0f : 0.0f; @@ -835,13 +510,13 @@ namespace Barotrauma targetMusic[mainTrackIndex] = null; } //switch the music if nothing playing atm or the currently playing clip is not suitable anymore - else if (targetMusic[mainTrackIndex] == null || currentMusic[mainTrackIndex] == null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m.File == currentMusic[mainTrackIndex].Filename)) + else if (targetMusic[mainTrackIndex] == null || currentMusic[mainTrackIndex] == null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m == currentMusic[mainTrackIndex])) { if (currentMusicType == "default") { if (previousDefaultMusic == null) { - targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandom(); + targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandomUnsynced(); } else { @@ -850,7 +525,7 @@ namespace Barotrauma } else { - targetMusic[mainTrackIndex] = suitableMusic.GetRandom(); + targetMusic[mainTrackIndex] = suitableMusic.GetRandomUnsynced(); } } @@ -858,16 +533,16 @@ namespace Barotrauma { // Find background noise loop for the current biome IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips(Level.Loaded.LevelData?.Biome?.Identifier, currentIntensity) : + GetSuitableMusicClips(Level.Loaded.LevelData.Biome.Identifier, currentIntensity) : Enumerable.Empty(); if (suitableNoiseLoops.Count() == 0) { targetMusic[noiseLoopIndex] = null; } // Switch the noise loop if nothing playing atm or the currently playing clip is not suitable anymore - else if (targetMusic[noiseLoopIndex] == null || currentMusic[noiseLoopIndex] == null || !suitableNoiseLoops.Any(m => m.File == currentMusic[noiseLoopIndex].Filename)) + else if (targetMusic[noiseLoopIndex] == null || currentMusic[noiseLoopIndex] == null || !suitableNoiseLoops.Any(m => m == currentMusic[noiseLoopIndex])) { - targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandom(); + targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandomUnsynced(); } } else @@ -875,35 +550,36 @@ namespace Barotrauma targetMusic[noiseLoopIndex] = null; } - IEnumerable suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience", currentIntensity); + IEnumerable suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience".ToIdentifier(), currentIntensity); int typeAmbienceTrackIndex = 2; if (suitableTypeAmbiences.None()) { targetMusic[typeAmbienceTrackIndex] = null; } // Switch the type ambience if nothing playing atm or the currently playing clip is not suitable anymore - else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m.File == currentMusic[typeAmbienceTrackIndex].Filename)) + else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m == currentMusic[typeAmbienceTrackIndex])) { - targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandom(); + targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); } //get the appropriate intensity layers for current situation IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips("intensity", currentIntensity) : + GetSuitableMusicClips("intensity".ToIdentifier(), currentIntensity) : Enumerable.Empty(); int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { //disable targetmusics that aren't suitable anymore - if (targetMusic[i] != null && !suitableIntensityMusic.Any(m => m.File == targetMusic[i].File)) + if (targetMusic[i] != null && !suitableIntensityMusic.Any(m => m == targetMusic[i])) { targetMusic[i] = null; } } + foreach (BackgroundMusic intensityMusic in suitableIntensityMusic) { //already playing, do nothing - if (targetMusic.Any(m => m != null && m.File == intensityMusic.File)) { continue; } + if (targetMusic.Any(m => m != null && m == intensityMusic)) { continue; } for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { @@ -932,12 +608,12 @@ namespace Barotrauma } } //something should be playing, but the targetMusic is invalid - else if (!musicClips.Any(mc => mc.File == targetMusic[i].File)) + else if (!musicClips.Any(mc => mc == targetMusic[i])) { - targetMusic[i] = GetSuitableMusicClips(targetMusic[i].Type, 0.0f).GetRandom(); + targetMusic[i] = GetSuitableMusicClips(targetMusic[i].Type, 0.0f).GetRandomUnsynced(); } //something should be playing, but the channel is playing nothing or an incorrect clip - else if (currentMusic[i] == null || targetMusic[i].File != currentMusic[i].Filename) + else if (currentMusic[i] == null || targetMusic[i] != currentMusic[i]) { //something playing -> mute it first if (musicChannel[i] != null && musicChannel[i].IsPlaying) @@ -949,18 +625,9 @@ namespace Barotrauma if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying)) { DisposeMusicChannel(i); - try - { - currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true); - } - catch (System.IO.InvalidDataException e) - { - DebugConsole.ThrowError($"Failed to load the music clip \"{targetMusic[i].File}\".", e); - musicClips.Remove(targetMusic[i]); - targetMusic[i] = null; - break; - } - musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "default" : "music"); + + currentMusic[i] = targetMusic[i]; + musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? "default" : "music"); if (targetMusic[i].ContinueFromPreviousTime) { musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; @@ -974,7 +641,7 @@ namespace Barotrauma if (musicChannel[i] == null || !musicChannel[i].IsPlaying) { musicChannel[i]?.Dispose(); - musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "default" : "music"); + musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? "default" : "music"); musicChannel[i].Looping = true; } float targetGain = targetMusic[i].Volume; @@ -989,17 +656,17 @@ namespace Barotrauma private static void DisposeMusicChannel(int index) { - var clip = musicClips.Find(m => m.File == musicChannel[index]?.Sound?.Filename); + var clip = musicClips.FirstOrDefault(m => m.Sound == musicChannel[index]?.Sound); if (clip != null) { if (clip.ContinueFromPreviousTime) { clip.PreviousTime = musicChannel[index].StreamSeekPos; } } musicChannel[index]?.Dispose(); musicChannel[index] = null; - currentMusic[index]?.Dispose(); currentMusic[index] = null; + currentMusic[index] = null; } - private static IEnumerable GetSuitableMusicClips(string musicType, float currentIntensity) + private static IEnumerable GetSuitableMusicClips(Identifier musicType, float currentIntensity) { return musicClips.Where(music => music != null && @@ -1008,28 +675,22 @@ namespace Barotrauma currentIntensity <= music.IntensityRange.Y); } - private static string GetCurrentMusicType() + private static Identifier GetCurrentMusicType() { if (OverrideMusicType != null) { return OverrideMusicType; } - if (Screen.Selected == null) { return "menu"; } + if (Screen.Selected == null) { return "menu".ToIdentifier(); } - if (Screen.Selected == GameMain.CharacterEditorScreen || - Screen.Selected == GameMain.LevelEditorScreen || - Screen.Selected == GameMain.ParticleEditorScreen || - Screen.Selected == GameMain.SpriteEditorScreen || - Screen.Selected == GameMain.SubEditorScreen || - Screen.Selected == GameMain.EventEditorScreen || - (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is TestGameMode) || - Screen.Selected == GameMain.NetLobbyScreen) + if ((Screen.Selected?.IsEditor ?? false) + || (Screen.Selected == GameMain.NetLobbyScreen)) { - return "editor"; + return "editor".ToIdentifier(); } if (Screen.Selected != GameMain.GameScreen) { previousDefaultMusic = null; - return firstTimeInMainMenu ? "menu" : "default"; + return (firstTimeInMainMenu ? "menu" : "default").ToIdentifier(); } firstTimeInMainMenu = false; @@ -1040,21 +701,21 @@ namespace Barotrauma if (Level.Loaded != null && Level.Loaded.Ruins != null && Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) { - return "ruins"; + return "ruins".ToIdentifier(); } if (Character.Controlled.Submarine?.Info?.IsWreck ?? false) { - return "wreck"; + return "wreck".ToIdentifier(); } if (Level.IsLoadedOutpost) { // Only return music type for location types which have music tracks defined - var locationType = Level.Loaded.StartLocation?.Type?.Identifier?.ToLowerInvariant(); - if (!string.IsNullOrEmpty(locationType) && musicClips.Any(c => c.Type == locationType)) + var locationType = Level.Loaded.StartLocation?.Type?.Identifier; + if (locationType.HasValue && locationType != Identifier.Empty && musicClips.Any(c => c.Type == locationType)) { - return locationType; + return locationType.Value; } } } @@ -1062,26 +723,26 @@ namespace Barotrauma Submarine targetSubmarine = Character.Controlled?.Submarine; if (targetSubmarine != null && targetSubmarine.AtDamageDepth) { - return "deep"; + return "deep".ToIdentifier(); } if (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Submarine.MainSub != null && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Submarine.MainSub.RealWorldCrushDepth) { - return "deep"; + return "deep".ToIdentifier(); } if (targetSubmarine != null) { float floodedArea = 0.0f; float totalArea = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != targetSubmarine) { continue; } floodedArea += hull.WaterVolume; totalArea += hull.Volume; } - if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded"; } + if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded".ToIdentifier(); } } float enemyDistThreshold = 5000.0f; @@ -1100,14 +761,14 @@ namespace Barotrauma { if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { - return "monster"; + return "monster".ToIdentifier(); } } else if (Character.Controlled != null) { if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { - return "monster"; + return "monster".ToIdentifier(); } } } @@ -1116,16 +777,16 @@ namespace Barotrauma { if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - return "levelend"; + return "levelend".ToIdentifier(); } if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0 && Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { - return "start"; + return "start".ToIdentifier(); } } - return "default"; + return "default".ToIdentifier(); } public static bool ShouldMuffleSound(Character listener, Vector2 soundWorldPos, float range, Hull hullGuess) @@ -1159,7 +820,7 @@ namespace Barotrauma if (SplashSounds.Count == 0) { return; } int splashIndex = MathHelper.Clamp((int)(strength + Rand.Range(-2.0f, 2.0f)), 0, SplashSounds.Count - 1); float range = 800.0f; - var channel = SplashSounds[splashIndex].Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); + var channel = SplashSounds[splashIndex].Sound.Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); } public static void PlayDamageSound(string damageType, float damage, PhysicsBody body) @@ -1169,35 +830,29 @@ namespace Barotrauma } private static readonly List tempList = new List(); - public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) + public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) { damage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); tempList.Clear(); foreach (var s in damageSounds) { - if ((s.damageRange == Vector2.Zero || - (damage >= s.damageRange.X && damage <= s.damageRange.Y)) && - string.Equals(s.damageType, damageType, StringComparison.OrdinalIgnoreCase) && - (string.IsNullOrEmpty(s.requiredTag) || (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag)))) + if ((s.DamageRange == Vector2.Zero || + (damage >= s.DamageRange.X && damage <= s.DamageRange.Y)) && + s.DamageType == damageType && + (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))) { tempList.Add(s); } } - var damageSound = tempList.GetRandom(); - if (damageSound.sound != null) - { - damageSound.sound.Play(1.0f, range, position, muffle: !damageSound.ignoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); - } + var damageSound = tempList.GetRandomUnsynced(); + damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); } public static void PlayUISound(GUISoundType soundType) { - if (guiSounds == null || guiSounds.Count < 1) { return; } - if (guiSounds.TryGetValue(soundType, out List sounds)) - { - if (sounds == null || sounds.Count < 1) { return; } - sounds.GetRandom()?.Play(null, "ui"); - } + GUISound.GUISoundPrefabs + .Where(s => s.Type == soundType) + .GetRandomUnsynced()?.Sound?.Play(null, "ui"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs new file mode 100644 index 000000000..8d510a115 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -0,0 +1,267 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + public class TagNames : Attribute + { + public readonly ImmutableHashSet Names; + + public TagNames(params string[] names) + { + Names = names.Select(n => n.ToIdentifier()).ToImmutableHashSet(); + } + } + + class SoundPrefab : Prefab + { + private class PrefabCollectionHandler + { + public readonly object Collection; + public readonly MethodInfo AddMethod; + public readonly MethodInfo RemoveMethod; + public readonly MethodInfo SortAllMethod; + public readonly MethodInfo AddOverrideFileMethod; + public readonly MethodInfo RemoveOverrideFileMethod; + + public void Add(SoundPrefab p, bool isOverride) + { + AddMethod.Invoke(Collection, new object[] { p, isOverride }); + } + + public void Remove(SoundPrefab p) + { + RemoveMethod.Invoke(Collection, new object[] { p }); + } + + public void AddOverrideFile(ContentFile file) + { + AddOverrideFileMethod.Invoke(Collection, new object[] { file }); + } + + public void RemoveOverrideFile(ContentFile file) + { + RemoveOverrideFileMethod.Invoke(Collection, new object[] { file }); + } + + public void SortAll() + { + SortAllMethod.Invoke(Collection, null); + } + + public PrefabCollectionHandler(Type type) + { + var collectionField = type.GetField($"{type.Name}Prefabs", BindingFlags.Public | BindingFlags.Static); + if (collectionField is null) { throw new InvalidOperationException($"Couldn't determine PrefabCollection for {type.Name}"); } + Collection = collectionField.GetValue(null) ?? throw new InvalidOperationException($"PrefabCollection for {type.Name} was null"); + AddMethod = Collection.GetType().GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); + RemoveMethod = Collection.GetType().GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance); + AddOverrideFileMethod = Collection.GetType().GetMethod("AddOverrideFile", BindingFlags.Public | BindingFlags.Instance); + RemoveOverrideFileMethod = Collection.GetType().GetMethod("RemoveOverrideFile", BindingFlags.Public | BindingFlags.Instance); + SortAllMethod = Collection.GetType().GetMethod("SortAll", BindingFlags.Public | BindingFlags.Instance); + } + } + + public readonly static PrefabSelector WaterAmbienceIn = new PrefabSelector(); + public readonly static PrefabSelector WaterAmbienceOut = new PrefabSelector(); + public readonly static PrefabSelector WaterAmbienceMoving = new PrefabSelector(); + public readonly static PrefabSelector StartupSound = new PrefabSelector(); + + private readonly static List flowSounds = new List(); + public static IReadOnlyList FlowSounds => flowSounds; + private readonly static List splashSounds = new List(); + public static IReadOnlyList SplashSounds => splashSounds; + + public readonly static ImmutableDictionary TagToDerivedPrefab; + private readonly static ImmutableDictionary derivedPrefabCollections; + private readonly static ImmutableDictionary> prefabSelectors; + private readonly static ImmutableDictionary> prefabsWithTag; + public readonly static PrefabCollection Prefabs; + + static SoundPrefab() + { + var types = ReflectionUtils.GetDerivedNonAbstract(); + //types.ForEach(t => t.GetProperties(BindingFlags.Public | BindingFlags.Static)); + TagToDerivedPrefab = types.SelectMany(t => + t.GetCustomAttribute().Names.Select(n => (n, t))).ToImmutableDictionary(); + derivedPrefabCollections = types.Select(t => (t, new PrefabCollectionHandler(t))).ToImmutableDictionary(); + + var prefabSelectorFields = typeof(SoundPrefab).GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.FieldType == typeof(PrefabSelector)); + prefabSelectors = prefabSelectorFields.Select(f => (f.Name.ToIdentifier(), (PrefabSelector)f.GetValue(null))).ToImmutableDictionary(); + + var prefabsOfTagName = typeof(SoundPrefab).GetFields(BindingFlags.Static | BindingFlags.NonPublic) + .Where(f => f.FieldType == typeof(List)); + prefabsWithTag = prefabsOfTagName.Select(f => (f.Name.Substring(0, f.Name.Length-6).ToIdentifier(), (List)f.GetValue(null))).ToImmutableDictionary(); + + Prefabs = new PrefabCollection( + onAdd: (SoundPrefab p, bool isOverride) => + { + if (derivedPrefabCollections.ContainsKey(p.GetType())) + { + derivedPrefabCollections[p.GetType()].Add(p, isOverride); + } + if (prefabSelectors.ContainsKey(p.ElementName)) { prefabSelectors[p.ElementName].Add(p, isOverride); } + UpdateSoundsWithTag(); + }, + onRemove: (SoundPrefab p) => + { + if (derivedPrefabCollections.ContainsKey(p.GetType())) + { + derivedPrefabCollections[p.GetType()].Remove(p); + } + if (prefabSelectors.ContainsKey(p.ElementName)) { prefabSelectors[p.ElementName].RemoveIfContains(p); } + UpdateSoundsWithTag(); + }, + onSort: () => + { + derivedPrefabCollections.Values.ForEach(h => h.SortAll()); + prefabSelectors.Values.ForEach(h => h.Sort()); + }, + onAddOverrideFile: (file) => {derivedPrefabCollections.Values.ForEach(h => h.AddOverrideFile(file)); }, + onRemoveOverrideFile: (file) => { derivedPrefabCollections.Values.ForEach(h => h.RemoveOverrideFile(file)); } + ); + } + + private static void UpdateSoundsWithTag() + { + foreach (var tag in prefabsWithTag.Keys) + { + var list = prefabsWithTag[tag]; + list.Clear(); + list.AddRange(Prefabs.Where(p => p.ElementName == tag)); + list.Sort((p1, p2) => + { + if (p1.ContentFile.ContentPackage.Index < p2.ContentFile.ContentPackage.Index) { return -1; } + if (p1.ContentFile.ContentPackage.Index > p2.ContentFile.ContentPackage.Index) { return 1; } + if (p2.Element.ComesAfter(p1.Element)) { return -1; } + if (p1.Element.ComesAfter(p2.Element)) { return 1; } + return 0; + }); + } + } + + protected override Identifier DetermineIdentifier(XElement element) + { + Identifier id = base.DetermineIdentifier(element); + if (id.IsEmpty) + { + if (id.IsEmpty) { id = Path.GetFileNameWithoutExtension(element.GetAttributeStringUnrestricted("path", "")).ToIdentifier(); } + if (id.IsEmpty) { id = Path.GetFileNameWithoutExtension(element.GetAttributeStringUnrestricted("file", "")).ToIdentifier(); } + + if (!id.IsEmpty) + { + id = $"{element.Name}_{id}".ToIdentifier(); + + string damageSoundType = element.GetAttributeString("damagesoundtype", ""); + if (!damageSoundType.IsNullOrEmpty()) + { + id = $"{id}_{damageSoundType}".ToIdentifier(); + } + + string musicType = element.GetAttributeString("type", ""); + if (!musicType.IsNullOrEmpty()) + { + id = $"{id}_{musicType}".ToIdentifier(); + } + } + } + + return id; + } + + public readonly ContentPath SoundPath; + public readonly ContentXElement Element; + public readonly Identifier ElementName; + public Sound Sound { get; private set; } + + public SoundPrefab(ContentXElement element, SoundsFile file, bool stream = false) : base(file, element) + { + SoundPath = element.GetAttributeContentPath("file") ?? ContentPath.Empty; + Element = element; + ElementName = element.NameAsIdentifier(); + Sound = GameMain.SoundManager.LoadSound(element, stream: stream); + } + + public bool IsPlaying() + { + return Sound.IsPlaying(); + } + + public override void Dispose() + { + Sound?.Dispose(); Sound = null; + } + } + + [TagNames("damagesound")] + class DamageSound : SoundPrefab + { + public readonly static PrefabCollection DamageSoundPrefabs = new PrefabCollection(); + + //the range of inflicted damage where the sound can be played + //(10.0f, 30.0f) would be played when the inflicted damage is between 10 and 30 + public readonly Vector2 DamageRange; + + public readonly Identifier DamageType; + + public readonly Identifier RequiredTag; + + public bool IgnoreMuffling; + + public DamageSound(ContentXElement element, SoundsFile file) : base(element, file) + { + DamageRange = element.GetAttributeVector2("damagerange", Vector2.Zero); + DamageType = element.GetAttributeIdentifier("damagesoundtype", "None"); + IgnoreMuffling = element.GetAttributeBool("ignoremuffling", false); + RequiredTag = element.GetAttributeIdentifier("requiredtag", ""); + } + } + + [TagNames("music")] + class BackgroundMusic : SoundPrefab + { + public readonly static PrefabCollection BackgroundMusicPrefabs = new PrefabCollection(); + + public readonly Identifier Type; + public readonly bool DuckVolume; + public readonly float Volume; + + public readonly Vector2 IntensityRange; + + public readonly bool ContinueFromPreviousTime; + public int PreviousTime; + + public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true) + { + Type = element.GetAttributeIdentifier("type", ""); + IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); + DuckVolume = element.GetAttributeBool("duckvolume", false); + this.Volume = element.GetAttributeFloat("volume", 1.0f); + ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); + } + } + + [TagNames("guisound")] + class GUISound : SoundPrefab + { + //public readonly static Dictionary> GUISoundsByType = new Dictionary>(); + public readonly static PrefabCollection GUISoundPrefabs = new PrefabCollection(); + + public readonly GUISoundType Type; + + public GUISound(ContentXElement element, SoundsFile file) : base(element, file) + { + Type = element.GetAttributeEnum("guisoundtype", GUISoundType.UIMessage); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index e7f1c6aad..92394a518 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Sounds { if (soundChannel == null) { return; } gain = value; - soundChannel.Gain = value * GameMain.Config.VoiceChatVolume; + soundChannel.Gain = value * GameSettings.CurrentConfig.Audio.VoiceChatVolume; } } @@ -105,9 +105,9 @@ namespace Barotrauma.Sounds { float fVal = ShortToFloat(buffer[i]); - if (gain * GameMain.Config.VoiceChatVolume > 1.0f) //TODO: take distance into account? + if (gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume > 1.0f) //TODO: take distance into account? { - fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1f, 1f); + fVal = Math.Clamp(fVal * gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume, -1f, 1f); } if (UseMuffleFilter) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index ae7ed3b35..765fa93ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Xml.Linq; namespace Barotrauma @@ -18,7 +19,7 @@ namespace Barotrauma } public string Name => $"Decorative Sprite"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } public Sprite Sprite { get; private set; } @@ -29,22 +30,22 @@ namespace Barotrauma Noise } - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 Offset { get; private set; } - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 RandomOffset { get; private set; } - [Serialize(AnimationType.None, false), Editable] + [Serialize(AnimationType.None, IsPropertySaveable.No), Editable] public AnimationType OffsetAnim { get; private set; } - [Serialize(0.0f, true), Editable] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float OffsetAnimSpeed { get; private set; } private float rotationSpeedRadians; private float absRotationSpeedRadians; - [Serialize(0.0f, true), Editable] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float RotationSpeed { get @@ -59,7 +60,7 @@ namespace Barotrauma } private float rotationRadians; - [Serialize(0.0f, true), Editable] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float Rotation { get @@ -73,7 +74,7 @@ namespace Barotrauma } private Vector2 randomRotationRadians; - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 RandomRotation { get @@ -87,30 +88,30 @@ namespace Barotrauma } private float scale; - [Serialize(1.0f, true), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable] public float Scale { get { return scale; } private set { scale = MathHelper.Clamp(value, 0.0f, 10.0f); } } - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 RandomScale { get; private set; } - [Serialize(AnimationType.None, false), Editable] + [Serialize(AnimationType.None, IsPropertySaveable.No), Editable] public AnimationType RotationAnim { get; private set; } /// /// If > 0, only one sprite of the same group is used (chosen randomly) /// - [Serialize(0, false, description: "If > 0, only one sprite of the same group is used (chosen randomly)"), Editable(ReadOnly = true)] + [Serialize(0, IsPropertySaveable.No, description: "If > 0, only one sprite of the same group is used (chosen randomly)"), Editable(ReadOnly = true)] public int RandomGroupID { get; private set; } - [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } /// @@ -122,12 +123,12 @@ namespace Barotrauma /// internal List AnimationConditionals { get; private set; } = new List(); - public DecorativeSprite(XElement element, string path = "", string file = "", bool lazyLoad = false) + public DecorativeSprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false) { Sprite = new Sprite(element, path, file, lazyLoad: lazyLoad); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); // load property conditionals - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { //choose which list the new conditional should be placed to List conditionalList = null; @@ -217,18 +218,18 @@ namespace Barotrauma return MathHelper.Lerp(RandomScale.X, RandomScale.Y, randomScaleModifier); } - public static void UpdateSpriteStates(Dictionary> spriteGroups, Dictionary animStates, + public static void UpdateSpriteStates(ImmutableDictionary> spriteGroups, Dictionary animStates, int entityID, float deltaTime, Func checkConditional) { foreach (int spriteGroup in spriteGroups.Keys) { - for (int i = 0; i < spriteGroups[spriteGroup].Count; i++) + for (int i = 0; i < spriteGroups[spriteGroup].Length; i++) { var decorativeSprite = spriteGroups[spriteGroup][i]; if (decorativeSprite == null) { continue; } if (spriteGroup > 0) { - int activeSpriteIndex = entityID % spriteGroups[spriteGroup].Count; + int activeSpriteIndex = entityID % spriteGroups[spriteGroup].Length; if (i != activeSpriteIndex) { animStates[decorativeSprite].IsActive = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs index 91175d95a..2f9b72f5b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs @@ -9,11 +9,11 @@ namespace Barotrauma.SpriteDeformations class CustomDeformationParams : SpriteDeformationParams { [Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f), - Serialize(0.0f, true, description: "How fast the deformation \"oscillates\" back and forth. " + + Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the deformation \"oscillates\" back and forth. " + "For example, if the sprite is stretched up, setting this value above zero would make it do a wave-like movement up and down.")] public override float Frequency { get; set; } = 1; - [Serialize(1.0f, true, description: "The \"strength\" of the deformation."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The \"strength\" of the deformation."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float Amplitude { get; set; } public CustomDeformationParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs index 6b7d599dd..5f4ef5f80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs @@ -6,9 +6,9 @@ namespace Barotrauma.SpriteDeformations { class InflateParams : SpriteDeformationParams { - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1)] public override float Frequency { get; set; } = 1; - [Serialize(1.0f, true), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.1f)] public float Scale { get; set; } public InflateParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs index 3830daaa1..775aac2ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs @@ -5,13 +5,13 @@ namespace Barotrauma.SpriteDeformations { class NoiseDeformationParams : SpriteDeformationParams { - [Serialize(0.0f, true, description: "The frequency of the noise."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The frequency of the noise."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1f)] public override float Frequency { get; set; } - [Serialize(1.0f, true, description: "How much the noise distorts the sprite."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the noise distorts the sprite."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] public float Amplitude { get; set; } - [Serialize(0.0f, true, description: "How fast the noise changes."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the noise changes."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] public float ChangeSpeed { get; set; } public NoiseDeformationParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs index 6901234d1..26ad4c949 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs @@ -10,26 +10,26 @@ namespace Barotrauma.SpriteDeformations /// 0 = no falloff, the entire sprite is stretched /// 1 = stretching the center of the sprite has no effect at the edges /// - [Serialize(0.0f, true, description: "0 = no falloff, the entire sprite is stretched, 1 = stretching the center of the sprite has no effect at the edges."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "0 = no falloff, the entire sprite is stretched, 1 = stretching the center of the sprite has no effect at the edges."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Falloff { get; set; } /// /// Maximum stretch per vertex (1 = the size of the sprite) /// - [Serialize(1.0f, true, description: "Maximum stretch per vertex (1 = the size of the sprite)"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Maximum stretch per vertex (1 = the size of the sprite)"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float MaxDeformation { get; set; } /// /// How fast the sprite reacts to being stretched /// - [Serialize(10.0f, true, description: "How fast the sprite reacts to being stretched"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the sprite reacts to being stretched"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float ReactionSpeed { get; set; } /// /// How fast the sprite returns back to normal after stretching ends /// - [Serialize(0.05f, true, description: "How fast the sprite returns back to normal after stretching ends"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.05f, IsPropertySaveable.Yes, description: "How fast the sprite returns back to normal after stretching ends"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float RecoverSpeed { get; set; } public PositionalDeformationParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs index 118734f9e..d15eb2729 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs @@ -14,21 +14,21 @@ namespace Barotrauma.SpriteDeformations /// A positive value means that this deformation is or could be used for multiple sprites. /// This behaviour is not automatic, and has to be implemented for any particular case separately (currently only used in Limbs). /// - [Serialize(-1, true), Editable(minValue: -1, maxValue: 100)] + [Serialize(-1, IsPropertySaveable.Yes), Editable(minValue: -1, maxValue: 100)] public int Sync { get; private set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string TypeName { get; set; } - [Serialize(SpriteDeformation.DeformationBlendMode.Add, true), Editable] + [Serialize(SpriteDeformation.DeformationBlendMode.Add, IsPropertySaveable.Yes), Editable] public SpriteDeformation.DeformationBlendMode BlendMode { get; @@ -37,30 +37,30 @@ namespace Barotrauma.SpriteDeformations public string Name => $"Deformation ({TypeName})"; - [Serialize(1.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float Strength { get; private set; } - [Serialize(90f, true), Editable(MinValueFloat = 0, MaxValueFloat = 90)] + [Serialize(90f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 90)] public float MaxRotation { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseMovementSine { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool StopWhenHostIsDead { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool OnlyInWater { get; set; } /// /// Only used if UseMovementSine is enabled. Multiplier for Pi. /// - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float SineOffset { get; set; } public virtual float Frequency { get; set; } = 1; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; @@ -72,7 +72,7 @@ namespace Barotrauma.SpriteDeformations public static readonly Point ShaderMaxResolution = new Point(15, 15); private Point _resolution; - [Serialize("2,2", true)] + [Serialize("2,2", IsPropertySaveable.Yes)] public Point Resolution { get { return _resolution; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs index 45bcb951b..7b89b66bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs @@ -345,7 +345,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Point(container.Rect.Width, (int)(60 * GUI.Scale)), container.RectTransform) { IsFixedSize = true }, - "Sprite Deformations", textAlignment: Alignment.BottomCenter, font: GUI.LargeFont); + "Sprite Deformations", textAlignment: Alignment.BottomCenter, font: GUIStyle.LargeFont); var resolutionField = GUI.CreatePointField(new Point(subDivX + 1, subDivY + 1), (int)(30 * GUI.Scale), "Resolution", container.RectTransform, "How many vertices the deformable sprite has on the x and y axes. Larger values make the deformations look smoother, but are more performance intensive."); @@ -387,7 +387,7 @@ namespace Barotrauma foreach (SpriteDeformation deformation in deformations) { var deformEditor = new SerializableEntityEditor(container.RectTransform, deformation.Params, - inGame: false, showName: true, titleFont: GUI.SubHeadingFont); + inGame: false, showName: true, titleFont: GUIStyle.SubHeadingFont); deformEditor.RectTransform.MinSize = new Point(deformEditor.Rect.Width, deformEditor.Rect.Height); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 818255063..8336320d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -10,11 +10,19 @@ namespace Barotrauma { public partial class Sprite { + private class TextureRefCounter + { + public Texture2D Texture; + public int RefCount; + } + + private readonly static Dictionary textureRefCounts = new Dictionary(); + private bool cannotBeLoaded; protected volatile bool loadingAsync = false; - - protected Texture2D texture; + + protected Texture2D texture { get; private set; } public Texture2D Texture { get @@ -24,6 +32,8 @@ namespace Barotrauma } } + private string disposeStackTrace; + public bool Loaded { get { return texture != null && !cannotBeLoaded; } @@ -32,7 +42,6 @@ namespace Barotrauma public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation) { FilePath = other.FilePath; - FullPath = other.FullPath; Compress = other.Compress; size = other.size; effects = other.effects; @@ -47,18 +56,13 @@ namespace Barotrauma origin = Vector2.Zero; effects = SpriteEffects.None; rotation = newRotation; - FilePath = path; + FilePath = ContentPath.FromRaw(path); AddToList(this); } partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) { - texture = LoadTexture(this.FilePath, out Sprite reusedSprite, Compress); - if (reusedSprite != null) - { - FilePath = string.Intern(reusedSprite.FilePath); - FullPath = string.Intern(reusedSprite.FullPath); - } + texture = LoadTexture(FilePath.Value, Compress); if (texture == null) { @@ -118,7 +122,7 @@ namespace Barotrauma public void ReloadTexture(IEnumerable spritesToUpdate) { texture.Dispose(); - texture = TextureLoader.FromFile(FilePath, Compress); + texture = TextureLoader.FromFile(FilePath.Value, Compress); foreach (Sprite sprite in spritesToUpdate) { sprite.texture = texture; @@ -130,14 +134,8 @@ namespace Barotrauma sourceRect = new Rectangle(0, 0, texture.Width, texture.Height); } - public static Texture2D LoadTexture(string file) + public static Texture2D LoadTexture(string file, bool compress = true) { - return LoadTexture(file, out _); - } - - public static Texture2D LoadTexture(string file, out Sprite reusedSprite, bool compress = true) - { - reusedSprite = null; if (string.IsNullOrWhiteSpace(file)) { Texture2D t = null; @@ -147,9 +145,15 @@ namespace Barotrauma }); return t; } - string fullPath = Path.GetFullPath(file); - reusedSprite = FindMatchingSprite(fullPath, requireTexture: true); - if (reusedSprite != null) { return reusedSprite.texture; } + Identifier fullPath = Path.GetFullPath(file).CleanUpPathCrossPlatform(correctFilenameCase: false).ToIdentifier(); + lock (list) + { + if (textureRefCounts.ContainsKey(fullPath)) + { + textureRefCounts[fullPath].RefCount++; + return textureRefCounts[fullPath].Texture; + } + } if (File.Exists(file)) { @@ -159,7 +163,13 @@ namespace Barotrauma DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!"); #endif } - return TextureLoader.FromFile(file, compress); + + Texture2D newTexture = TextureLoader.FromFile(file, compress); + lock (list) + { + textureRefCounts.Add(fullPath, new TextureRefCounter { RefCount = 1, Texture = newTexture }); + } + return newTexture; } else { @@ -170,22 +180,6 @@ namespace Barotrauma return null; } - private static Sprite FindMatchingSprite(string fullPath, bool requireTexture) - { - lock (list) - { - foreach (var wRef in list) - { - if (wRef.TryGetTarget(out Sprite sprite)) - { - bool hasTexture = sprite.texture != null && !sprite.texture.IsDisposed; - if (sprite.FullPath == fullPath && (hasTexture || !requireTexture)) { return sprite; } - } - } - } - return null; - } - public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None) { this.Draw(spriteBatch, pos, Color.White, rotate, scale, spriteEffect); @@ -378,15 +372,33 @@ namespace Barotrauma partial void DisposeTexture() { - //check if another sprite is using the same texture - if (!string.IsNullOrEmpty(FilePath)) //file can be empty if the sprite is created directly from a Texture2D instance - { - if (FindMatchingSprite(FullPath, requireTexture: false) != null) { return; } - } - - //if not, free the texture + disposeStackTrace = Environment.StackTrace; if (texture != null) { + //check if another sprite is using the same texture + lock (list) + { + if (!FilePath.IsNullOrEmpty()) //file can be empty if the sprite is created directly from a Texture2D instance + { + Identifier pathKey = FullPath.ToIdentifier(); + if (!pathKey.IsEmpty && textureRefCounts.ContainsKey(pathKey)) + { + textureRefCounts[pathKey].RefCount--; + if (textureRefCounts[pathKey].RefCount <= 0) + { + textureRefCounts.Remove(pathKey); + } + else + { + texture = null; + FilePath = ContentPath.Empty; + return; + } + } + } + } + + //if not, free the texture CrossThread.RequestExecutionOnMainThread(() => { texture.Dispose(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 2b06523f6..ca6ea6874 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -24,11 +24,11 @@ namespace Barotrauma private double loopStartTime; private bool loopSound; - partial void InitProjSpecific(XElement element, string parentDebugName) + partial void InitProjSpecific(ContentXElement element, string parentDebugName) { particleEmitters = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -36,7 +36,7 @@ namespace Barotrauma particleEmitters.Add(new ParticleEmitter(subElement)); break; case "sound": - var sound = Submarine.LoadRoundSound(subElement); + var sound = RoundSound.Load(subElement); if (sound?.Sound != null) { loopSound = subElement.GetAttributeBool("loop", false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs new file mode 100644 index 000000000..82fa24ac1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs @@ -0,0 +1,27 @@ +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; + + DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); + Steamworks.BeginAuthResult startResult = Steamworks.SteamUser.BeginAuthSession(authTicketData, clientSteamID); + if (startResult != Steamworks.BeginAuthResult.OK) + { + DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); + } + + return startResult; + } + + public static void StopAuthSession(ulong clientSteamID) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) return; + + DebugConsole.NewMessage("SteamManager ending auth session with Steam client " + clientSteamID); + Steamworks.SteamUser.EndAuthSession(clientSteamID); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs new file mode 100644 index 000000000..93b6b3235 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs @@ -0,0 +1,187 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private readonly struct BBWord + { + [Flags] + public enum TagType + { + None = 0x0, + Bold = 0x1, + Italic = 0x2, + Header = 0x4, + List = 0x8, + NewLine = 0x10 + } + + public readonly string Text; + public readonly Vector2 Size; + public readonly TagType TagTypes; + + public readonly GUIFont Font; + + public BBWord(string text, TagType tagTypes) + { + Text = text; + TagTypes = tagTypes; + Font = tagTypes.HasFlag(TagType.Header) + ? GUIStyle.LargeFont + : tagTypes.HasFlag(TagType.Bold) + ? GUIStyle.SubHeadingFont + : GUIStyle.Font; + Size = Font.MeasureString(Text); + } + } + + private static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static GUICustomComponent CreateBBCodeElement(string bbCode, GUIListBox container) + { + Point cachedContainerSize = Point.Zero; + List bbWords = new List(); + Stack tagStack = new Stack(); + + void recalculate() + { + if (cachedContainerSize == container.Content.RectTransform.NonScaledSize) { return; } + + bbWords.Clear(); + cachedContainerSize = container.Content.RectTransform.NonScaledSize; + + var matches = new Stack(bbTagRegex.Matches(bbCode).Reverse()); + Match? nextTag = null; + matches.TryPop(out nextTag); + int wordStart = 0; + BBWord.TagType currTagType; + for (int i = 0; i < bbCode.Length; i++) + { + char currChar = bbCode[i]; + currTagType = tagStack.TryPeek(out var t) ? t : BBWord.TagType.None; + + bool charIsCJK = TextManager.IsCJK($"{currChar}"); + bool wordEnd = char.IsWhiteSpace(currChar) || charIsCJK; + int reachedTagLength = 0; + if (nextTag is { Index: int tagIndex, Length: int tagLength } + && i == tagIndex) + { + reachedTagLength = tagLength; + string tagStr = nextTag.Value.Replace("[", "").Replace("]", "").Trim(); + bool isClosing = tagStr.StartsWith("/"); + tagStr = tagStr.Replace("/", "").Trim().ToLowerInvariant(); + BBWord.TagType tagType = tagStr switch + { + "b" => BBWord.TagType.Bold, + "i" => BBWord.TagType.Italic, + "h1" => BBWord.TagType.Header, + _ => BBWord.TagType.None + }; + + if (tagType != BBWord.TagType.None) + { + if (isClosing) + { + if (currTagType == tagType) + { + tagStack.Pop(); + } + } + else + { + tagStack.Push(tagType); + } + } + } + + if (wordEnd || reachedTagLength > 0) + { + string word = bbCode[wordStart..i]; + if (charIsCJK) { word = bbCode[wordStart..(i + 1)]; } + else if (char.IsWhiteSpace(currChar) && currChar != '\n') { word += " "; } + + if (!word.IsNullOrEmpty()) + { + bbWords.Add(new BBWord(word, currTagType)); + } + else if (currChar == '\n') + { + bbWords.Add(new BBWord("", BBWord.TagType.NewLine)); + } + + if (reachedTagLength > 0) + { + i += reachedTagLength - 1; + nextTag = matches.TryPop(out var tag) ? tag : null; + } + + wordStart = i + 1; + } + } + + currTagType = tagStack.TryPeek(out var ft) ? ft : BBWord.TagType.None; + string finalWord = bbCode[wordStart..]; + if (!finalWord.IsNullOrEmpty()) + { + bbWords.Add(new BBWord(finalWord, currTagType)); + } + } + + void draw(SpriteBatch spriteBatch, GUICustomComponent component) + { + recalculate(); + Vector2 currPos = Vector2.Zero; + Vector2 rectPos = component.Rect.Location.ToVector2(); + for (int i = 0; i < bbWords.Count; i++) + { + var bbWord = bbWords[i]; + if (currPos.X > 0.0f + && currPos.X + bbWord.Size.X >= component.Rect.Width) + { + //wrap because we went over width limit + currPos = (0.0f, currPos.Y + bbWord.Size.Y); + } + + bbWord.Font.DrawString( + spriteBatch, + bbWord.Text, + (currPos + rectPos).ToPoint().ToVector2(), + GUIStyle.TextColorNormal, + forceUpperCase: ForceUpperCase.No, + italics: bbWord.TagTypes.HasFlag(BBWord.TagType.Italic)); + bool breakLine + = bbWord.TagTypes.HasFlag(BBWord.TagType.NewLine) + || (i < bbWords.Count - 1 && + bbWords[i + 1].TagTypes.HasFlag(BBWord.TagType.Header) != + bbWord.TagTypes.HasFlag(BBWord.TagType.Header)); + if (breakLine) + { + //break line because of a header change or newline was found + currPos = (0.0f, currPos.Y + bbWord.Size.Y); + } + else + { + currPos.X += bbWord.Size.X; + } + } + + component.RectTransform.NonScaledSize + = (component.RectTransform.NonScaledSize.X, + (int)(currPos.Y + bbWords.LastOrDefault().Size.Y)); + component.RectTransform.RelativeSize + = component.RectTransform.NonScaledSize.ToVector2() / component.Parent.Rect.Size.ToVector2(); + } + + return new GUICustomComponent(new RectTransform(Vector2.One, container.Content.RectTransform), + onDraw: draw); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs new file mode 100644 index 000000000..9c3cfb54b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs @@ -0,0 +1,694 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using ItemOrPackage = Barotrauma.Either; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private string ExtractTitle(ItemOrPackage itemOrPackage) + => itemOrPackage.TryGet(out ContentPackage package) + ? package.Name + : ((Steamworks.Ugc.Item)itemOrPackage).Title; + + private void CreateWorkshopItemDetailContainer( + GUIFrame parent, + out GUIListBox outerContainer, + Action onSelected, + Action onDeselected, + out Action select, + out Action deselect) + { + ItemOrPackage? selectedItemOrPackage = null; + + GUIListBox outContainer = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform), + isHorizontal: true, + style: null) + { + ScrollBarEnabled = false, + ScrollBarVisible = false, + HoverCursor = CursorState.Default + }; + outerContainer = outContainer; + + var selectedLayout = + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)); + var selectedHeaderLayout = + new GUILayoutGroup(new RectTransform((1.0f, 0.05f), selectedLayout.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft); + + void deselectMethod() + { + if (selectedItemOrPackage is null) { return; } + selectedItemOrPackage = null; + onDeselected(); + } + + deselect = deselectMethod; + + var backButton = + new GUIButton(new RectTransform((0.04f, 1.0f), selectedHeaderLayout.RectTransform), + style: "GUIButtonToggleLeft") + { + OnClicked = (button, o) => + { + deselectMethod(); + return false; + } + }; + var padding = new GUIFrame(new RectTransform((1.0f, 0.005f), selectedLayout.RectTransform), style: null); + var selectedFrame = new GUIFrame(new RectTransform((1.0f, 0.945f), selectedLayout.RectTransform), + style: null); + + var selectionScroller = new GUICustomComponent( + new RectTransform(Vector2.Zero, outerContainer.Parent.RectTransform), + onUpdate: (deltaTime, component) => + { + float targetScroll = selectedItemOrPackage is null + ? 0.0f + : 1.0f; + outContainer.ScrollBar.BarScroll + = MathUtils.NearlyEqual(targetScroll, outContainer.ScrollBar.BarScroll) + ? targetScroll + : MathHelper.Lerp(outContainer.ScrollBar.BarScroll, targetScroll, 0.3f); + }); + + select = itemOrPackage => + { + //showInSteamButton.Visible = itemOrPackage.TryGet(out Steamworks.Ugc.Item _); + //selectedItem = itemOrPackage; + //selectedTitle.Text = ExtractTitle(itemOrPackage); + selectedFrame.ClearChildren(); + + //Jank to fix mouserect not clamping properly + //when shifting all elements to the left + var dropdowns = outContainer.Content.GetAllChildren().ToArray(); + var allChildren = outContainer.Content.GetAllChildren() + .Concat(selectedFrame.GetAllChildren()); + allChildren.ForEach(c => + { + //c.CascadingMouseRectClamp = !dropdowns.Any(dd => dd.IsParentOf(c) || dd.ListBox.IsParentOf(c)); + //c.CanBeFocused = c.CanBeFocused || !c.CascadingMouseRectClamp; + c.ClampMouseRectToParent = !(c.Parent?.Parent is GUIDropDown); + } + ); + + selectedItemOrPackage = itemOrPackage; + onSelected(itemOrPackage, selectedFrame); + }; + } + + private void CreateWorkshopItemList( + GUIFrame parent, + out GUIListBox outerContainer, + out GUIListBox workshopItemList, + Action onSelected) + => CreateWorkshopItemOrPackageList( + parent, + out outerContainer, + out workshopItemList, + onSelected: (ItemOrPackage itemOrPackage, GUIFrame frame) + => onSelected((Steamworks.Ugc.Item)itemOrPackage, frame)); + + private GUIButton CreateShowInSteamButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT) + => new GUIButton( + rectT, + TextManager.Get("WorkshopShowItemInSteam"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + SteamManager.OverlayCustomURL(workshopItem.Url); + return false; + } + }; + + private GUIButton? CreateShowInSteamButton(ItemOrPackage itemOrPackage) + => itemOrPackage.TryGet(out Steamworks.Ugc.Item workshopItem) + ? CreateShowInSteamButton(workshopItem) + : null; + + private void CreateWorkshopItemOrPackageList( + GUIFrame parent, + out GUIListBox outerContainer, + out GUIListBox workshopItemList, + Action onSelected) + { + GUIListBox? itemList = null; + + CreateWorkshopItemDetailContainer( + parent, + out outerContainer, + onSelected: onSelected, + onDeselected: () => itemList?.Deselect(), + out var select, out var deselect); + + itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)); + itemList.RectTransform.SetAsFirstChild(); + workshopItemList = itemList; + + var deselectCarrier + = CreateActionCarrier(outerContainer.Content, nameof(deselect).ToIdentifier(), deselect); + + itemList.OnSelected = (component, userData) => + { + //Don't select if hitting the subscribe button + if (GUI.MouseOn.Parent != itemList.Content) { return false; } + + if (!(userData is ItemOrPackage itemOrPackage)) { return false; } + + select(itemOrPackage); + + return true; + }; + } + + private void AddUnpublishedMods(ISet workshopItems) + { + //Users that don't have a proper license cannot publish Workshop items + //(see https://partner.steamgames.com/doc/features/workshop#15) + void clearWithMessage(LocalizedString message) + { + selfModsList.ClearChildren(); + var messageFrame = new GUIFrame(new RectTransform(Vector2.One, selfModsList.Content.RectTransform), + style: null) + { + CanBeFocused = false + }; + new GUITextBlock(new RectTransform((0.5f, 1.0f), messageFrame.RectTransform, Anchor.Center), + text: message, + textAlignment: Alignment.Center, + wrap: true, + font: GUIStyle.Font); + } + + if (SteamManager.IsFreeWeekend()) + { + clearWithMessage(TextManager.Get("FreeWeekendCantPublish")); + return; + } + if (SteamManager.IsFamilyShared()) + { + clearWithMessage(TextManager.Get("FamilySharedCantPublish")); + return; + } + + DateTime getEditTime(ContentPackage p) + => File.GetLastWriteTime(Path.GetDirectoryName(p.Path)!); + + //Find local packages associated with the Workshop items if available + (Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems + .Select(item => (item, + (ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p + => p.SteamWorkshopId != 0 && p.SteamWorkshopId == item.Id))) + //Sort the pairs by last local edit time if available + .OrderBy(t => t.Item2 == null) + .ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime) + .ToArray(); + + int indexOfUserDataInPublishedItemsArray(object userData) + => publishedItems.IndexOf(t + => t.WorkshopItem.Id == ((Steamworks.Ugc.Item)(userData as ItemOrPackage)).Id); + + //Take the existing GUI items that are in the list and sort to match the order of publishedItems + var publishedGuiComponents = selfModsList.Content.Children.OrderBy(c => indexOfUserDataInPublishedItemsArray(c.UserData)).ToArray(); + + //Get mods that haven't been published and add them to the list + var unpublishedMods = ContentPackageManager.LocalPackages + .Where(p => p.SteamWorkshopId == 0 || !publishedItems.Any(item => item.WorkshopItem.Id == p.SteamWorkshopId)) + .OrderByDescending(getEditTime).ToArray(); + + if (unpublishedMods.Any()) + { + var unpublishedHeader + = new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform), + TextManager.Get("UnpublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; + } + + foreach (var unpublishedMod in unpublishedMods) + { + var unpublishedFrame = new GUIFrame( + new RectTransform((1.0f, 1.0f / 5.5f), selfModsList.Content.RectTransform), + style: "ListBoxElement") + { + UserData = (ItemOrPackage)unpublishedMod + }; + var unpublishedLayout + = new GUILayoutGroup(new RectTransform(Vector2.One, unpublishedFrame.RectTransform), + isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + var unpublishedPadding + = new GUIFrame( + new RectTransform(Vector2.One, unpublishedLayout.RectTransform, + scaleBasis: ScaleBasis.BothHeight), style: null) + { + CanBeFocused = false + }; + var unpublishedTextBlock + = new GUITextBlock(new RectTransform(Vector2.One, unpublishedLayout.RectTransform), + $"{unpublishedMod.Name}\n\n" + + TextManager.GetWithVariable("LastLocalEditTime", + "[datetime]", + getEditTime(unpublishedMod).ToString()), + font: GUIStyle.Font) + { + CanBeFocused = false + }; + } + + if (publishedGuiComponents.Any()) + { + var publishedHeader + = new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform), + TextManager.Get("PublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; + } + + foreach (var c in publishedGuiComponents) + { + c.SetAsLastChild(); + var textBlock = (c.FindChild(b => b is GUITextBlock, recursive: true) as GUITextBlock)!; + textBlock.Text += $"\n"; + + int index = indexOfUserDataInPublishedItemsArray(c.UserData); + (Steamworks.Ugc.Item workshopItem, ContentPackage? localMod) = publishedItems[index]; + if (localMod != null) + { + textBlock.Text += $"\n" + TextManager.GetWithVariable("LastLocalEditTime", "[datetime]", getEditTime(localMod).ToString()); + } + textBlock.Text += $"\n" + TextManager.GetWithVariable("LatestPublishTime", "[datetime]", workshopItem.LatestUpdateTime.ToLocalTime().ToString()); + } + } + + private static (GUIButton Button, GUIFrame Sprite) CreatePaddedButton(RectTransform rectT, string style, float spriteScale) + { + var button = new GUIButton( + rectT, + style: null); + + var sprite = new GUIFrame( + new RectTransform(Vector2.One * spriteScale, button.RectTransform, Anchor.Center), + style: style) + { + CanBeFocused = false + }; + + return (button, sprite); + } + + private static void CreateSubscribeButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT, float spriteScale) + { + const string plusButton = "GUIPlusButton"; + const string minusButton = "GUIMinusButton"; + + LocalizedString subscribeTooltip = TextManager.Get("DownloadButton"); + LocalizedString unsubscribeTooptip = TextManager.Get("WorkshopItemUnsubscribe"); + + var (subscribeButton, subscribeButtonSprite) = CreatePaddedButton(rectT, plusButton, spriteScale); + subscribeButton.ToolTip = subscribeTooltip; + + subscribeButton.OnClicked = (button, o) => + { + if (!workshopItem.IsSubscribed) + { + workshopItem.Subscribe(); + TaskPool.Add($"DownloadSubscribedItem{workshopItem.Id}", + SteamManager.Workshop.ForceRedownload(workshopItem), + t => { }); + } + else + { + workshopItem.Unsubscribe(); + SteamManager.Workshop.Uninstall(workshopItem); + } + + return false; + }; + + var buttonStyleUpdater = new GUICustomComponent( + new RectTransform(Vector2.Zero, subscribeButton.RectTransform), + onUpdate: (deltaTime, component) => + { + if (subscribeButtonSprite.Style is { Identifier: { } styleId }) + { + if (workshopItem.IsSubscribed && styleId != minusButton) + { + subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(minusButton)); + subscribeButton.ToolTip = unsubscribeTooptip; + } + if (!workshopItem.IsSubscribed && styleId != plusButton) + { + subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(plusButton)); + subscribeButton.ToolTip = subscribeTooltip; + } + } + }); + + float displayedDownloadAmount = workshopItem.DownloadAmount; + var downloadProgressBar = new GUICustomComponent( + new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center), + onDraw: (spriteBatch, component) => + { + bool visible = workshopItem.IsSubscribed + && (workshopItem.IsDownloading + || workshopItem.IsDownloadPending + || !MathUtils.NearlyEqual(workshopItem.DownloadAmount, displayedDownloadAmount)); + if (!visible) { return; } + + void drawSection(float amount, Color color, float thickness) + => GUI.DrawDonutSection( + spriteBatch, + component.Rect.Center.ToVector2() + (0, 1), + new Range(component.Rect.Width * 0.55f - thickness * 0.5f, component.Rect.Width * 0.55f + thickness * 0.5f), + amount * MathF.PI * 2.0f, + color); + + void drawSectionFuzzy(float amount, Color color, float thickness) + { + drawSection(amount, color, thickness); + drawSection(amount, color * 0.6f, thickness + 0.5f); + drawSection(amount, color * 0.3f, thickness + 1.0f); + } + + drawSectionFuzzy(1.0f, Color.Lerp(Color.Black, GUIStyle.Blue, 0.2f), component.Rect.Width * 0.25f); + drawSectionFuzzy(1.0f, Color.Black, component.Rect.Width * 0.15f); + drawSectionFuzzy(displayedDownloadAmount, GUIStyle.Green, component.Rect.Width * 0.08f); + }, + onUpdate: (deltaTime, component) => + { + displayedDownloadAmount = Math.Min( + workshopItem.DownloadAmount, + MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f)); + }) + { + CanBeFocused = false + }; + } + + private void PopulateItemList(GUIListBox itemListBox, Task> items, bool includeSubscribeButton, Action>? onFill = null) + { + itemListBox.ClearChildren(); + itemListBox.Deselect(); + itemListBox.ScrollBar.BarScroll = 0.0f; + TaskPool.Add("PopulateTabWithItemList", items, + (t) => + { + taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; + itemListBox.ClearChildren(); + var workshopItems = ((Task>)t).Result; + foreach (var workshopItem in workshopItems) + { + var itemFrame = new GUIFrame( + new RectTransform((1.0f, 1.0f / 5.5f), itemListBox.Content.RectTransform), + style: "ListBoxElement") + { + UserData = (ItemOrPackage)workshopItem + }; + var itemLayout = new GUILayoutGroup( + new RectTransform(Vector2.One, itemFrame.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + var thumbnailContainer + = CreateThumbnailContainer(itemLayout, Vector2.One, ScaleBasis.BothHeight); + CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer); + thumbnailContainer.CanBeFocused = false; + thumbnailContainer.GetAllChildren().ForEach(c => c.CanBeFocused = false); + + var title = new GUITextBlock( + new RectTransform(Vector2.One, itemLayout.RectTransform), + workshopItem.Title, font: GUIStyle.Font) + { + CanBeFocused = false + }; + + if (includeSubscribeButton) + { + CreateSubscribeButton(workshopItem, new RectTransform(Vector2.One, itemLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), spriteScale: 0.4f); + } + } + onFill?.Invoke(workshopItems); + }); + } + + private GUIFrame CreateThumbnailContainer( + GUIComponent parent, + Vector2 relativeSize, + ScaleBasis scaleBasis) + => new GUIFrame(new RectTransform(relativeSize, parent.RectTransform, scaleBasis: scaleBasis), + style: "GUIFrameListBox"); + + private SteamManager.Workshop.ItemThumbnail CreateItemThumbnail( + in Steamworks.Ugc.Item workshopItem, + CancellationToken cancellationToken, + GUIFrame thumbnailContainer) + { + var thumbnail = new SteamManager.Workshop.ItemThumbnail(workshopItem, cancellationToken); + itemThumbnails.Add(thumbnail); + CreateAsyncThumbnailComponent(thumbnailContainer, () => thumbnail.Texture, () => thumbnail.Loading); + return thumbnail; + } + + private GUICustomComponent CreateAsyncThumbnailComponent(GUIFrame thumbnailContainer, Func textureGetter, Func throbberEnabled) + { + int randomThrobberOffset = Rand.Range(0, 10, Rand.RandSync.Unsynced); + return new GUICustomComponent( + new RectTransform(Vector2.One, thumbnailContainer.RectTransform, Anchor.Center), + onDraw: (spriteBatch, component) => + { + Rectangle rect = component.Rect; + Texture2D? texture = textureGetter(); + if (texture != null) + { + rect.Location += (4, 4); + rect.Size -= (8, 8); + Point destinationSizeMaxWidth = (rect.Width, rect.Width * texture.Height / texture.Width); + Point destinationSizeMaxHeight = (rect.Height * texture.Width / texture.Height, rect.Height); + Point destinationSize = destinationSizeMaxHeight.X > rect.Width + ? destinationSizeMaxWidth + : destinationSizeMaxHeight; + Rectangle destinationRectangle = new Rectangle( + rect.Center.X - destinationSize.X / 2, + rect.Center.Y - destinationSize.Y / 2, + destinationSize.X, + destinationSize.Y); + spriteBatch.Draw(texture, destinationRectangle, Color.White); + } + else if (throbberEnabled()) + { + var sheet = GUIStyle.GenericThrobber; + Vector2 pos = rect.Center.ToVector2() - Vector2.One * rect.Height * 0.4f; + sheet.Draw(spriteBatch, ((int)Math.Floor(Timing.TotalTime * 24.0f) + randomThrobberOffset) % sheet.FrameCount, pos, Color.White, + origin: Vector2.Zero, rotate: 0.0f, + scale: Vector2.One * component.Rect.Height / sheet.FrameSize.ToVector2() * 0.8f); + } + }); + } + + private GUIListBox CreateTagsList(IEnumerable tags, RectTransform rectT, bool canBeFocused) + { + var tagsList + = new GUIListBox(rectT, style: null, isHorizontal: false) + { + UseGridLayout = true, + ScrollBarEnabled = false, + ScrollBarVisible = false, + HideChildrenOutsideFrame = false, + Spacing = GUI.IntScale(4) + }; + tagsList.Content.ClampMouseRectToParent = false; + foreach (Identifier tag in tags) + { + var tagBtn = new GUIButton( + new RectTransform(new Vector2(0.25f, 1.0f / 8.0f), tagsList.Content.RectTransform, + anchor: Anchor.TopLeft), + TextManager.Get($"workshop.contenttag.{tag.Value.RemoveWhitespace()}") + .Fallback(tag.Value.CapitaliseFirstInvariant()), style: "GUIButtonRound") + { + CanBeFocused = canBeFocused, + Selected = !canBeFocused, + UserData = tag + }; + tagBtn.RectTransform.NonScaledSize + = tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(5)); + tagBtn.RectTransform.IsFixedSize = true; + tagBtn.ClampMouseRectToParent = false; + } + + return tagsList; + } + + private void PopulateFrameWithItemInfo(Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) + { + taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; + + var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform)); + + var headerLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.1f), verticalLayout.RectTransform), + isHorizontal: true) { Stretch = true }; + + var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform)); + + var selectedTitle = + new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title, + font: GUIStyle.LargeFont); + + var author = workshopItem.Owner; + var authorButton = new GUIButton(new RectTransform((1.0f, 0.5f), + titleAndAuthorLayout.RectTransform), + style: null, + textAlignment: Alignment.CenterLeft) + { + ForceUpperCase = ForceUpperCase.No, + Font = GUIStyle.SubHeadingFont, + TextColor = GUIStyle.TextColorNormal, + HoverTextColor = Color.White, + SelectedTextColor = GUIStyle.TextColorNormal, + OnClicked = (button, o) => + { + SteamManager.OverlayCustomURL( + $"https://steamcommunity.com/profiles/{author.Id}/myworkshopfiles/?appid={SteamManager.AppID}"); + return false; + } + }; + var authorPadding = authorButton.GetChild().Padding; + + RectTransform rightSideButtonRectT() + => new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight); + + var (reinstallButton, reinstallSprite) = CreatePaddedButton( + rightSideButtonRectT(), + "GUIReloadButton", + spriteScale: 0.8f); + reinstallButton.ToolTip = TextManager.Get("WorkshopItemReinstall"); + reinstallButton.OnClicked += (button, o) => + { + SteamManager.Workshop.Uninstall(workshopItem); + TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.ForceRedownload(workshopItem), t => { }); + return false; + }; + var reinstallButtonUpdater = new GUICustomComponent( + new RectTransform(Vector2.Zero, reinstallButton.RectTransform), + onUpdate: (f, component) => + { + reinstallButton.Visible = workshopItem.IsSubscribed; + reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending && + !SteamManager.Workshop.IsInstalling(workshopItem); + reinstallSprite.Color = reinstallButton.Enabled + ? reinstallSprite.Style.Color + : Color.DimGray; + }); + CreateSubscribeButton(workshopItem, + rightSideButtonRectT(), + spriteScale: 0.8f); + + var padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null); + + var horizontalLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.45f), verticalLayout.RectTransform), + isHorizontal: true) + { + Stretch = true + }; + + TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) => + { + authorButton.Text = author.Name; + authorButton.RectTransform.NonScaledSize = + ((int)(authorButton.Font.MeasureString(author.Name).X + authorPadding.X + authorPadding.Z), + authorButton.RectTransform.NonScaledSize.Y); + }); + + var thumbnailSuperContainer = new GUIFrame( + new RectTransform(Vector2.One, horizontalLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null); + GUIFrame thumbnailContainer = CreateThumbnailContainer(thumbnailSuperContainer, Vector2.One, + scaleBasis: ScaleBasis.BothHeight); + CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer); + thumbnailContainer.RectTransform.Anchor = Anchor.Center; + thumbnailContainer.RectTransform.Pivot = Pivot.Center; + + var statsBox = new GUIFrame(new RectTransform((0.6f, 1.0f), horizontalLayout.RectTransform), + style: "GUIFrameListBox"); + + #region Stats box + var statsHorizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, statsBox.RectTransform), isHorizontal: true); + var statsVertical0 + = new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform)); + + statFrame("", ""); //padding + + var scoreFrame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), style: null); + var scoreLabel = new GUITextBlock(new RectTransform((0.4f, 1.0f), scoreFrame.RectTransform), + TextManager.Get("WorkshopItemScore"), font: GUIStyle.SubHeadingFont); + var scoreStarContainer + = new GUILayoutGroup( + new RectTransform((0.6f, 1.0f), scoreFrame.RectTransform, Anchor.CenterRight), + isHorizontal: true, + childAnchor: Anchor.CenterLeft) { Stretch = true }; + var starColor = Color.Lerp( + Color.Lerp(Color.Red, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)), + Color.Lime, Math.Max(0.0f, (workshopItem.Score - 0.5f) * 2.0f)); + for (int i = 0; i < 5; i++) + { + bool isStarLit = i <= Round(workshopItem.Score * 5.0f); + var star = new GUIFrame(new RectTransform(Vector2.One, scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: isStarLit ? "GUIStarIconBright" : "GUIStarIconDark"); + if (isStarLit) + { + star.Color = starColor; + star.HoverColor = starColor; + star.SelectedColor = starColor; + } + } + var scoreVoteCountPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null); + var scoreVoteCount = new GUITextBlock( + new RectTransform(Vector2.One, scoreStarContainer.RectTransform), + TextManager.GetWithVariable("WorkshopItemVotes", "[VoteCount]", + (workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.CenterLeft) + { + Padding = Vector4.Zero + }; + + void statFrame(LocalizedString labelText, LocalizedString dataText) + { + var frame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0!.RectTransform), style: null); + var label = new GUITextBlock(new RectTransform((0.4f, 1.0f), frame.RectTransform), + labelText, font: GUIStyle.SubHeadingFont); + var data = new GUITextBlock(new RectTransform((0.6f, 1.0f), frame.RectTransform, Anchor.CenterRight), + dataText, font: GUIStyle.Font) + { + Padding = Vector4.Zero + }; + } + + statFrame(TextManager.Get("WorkshopItemFileSize"), MathUtils.GetBytesReadable(workshopItem.SizeOfFileInBytes)); + statFrame(TextManager.Get("WorkshopItemCreationDate"), workshopItem.Created.ToShortDateString()); + statFrame(TextManager.Get("WorkshopItemModificationDate"), workshopItem.Updated.ToShortDateString()); + + var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), + TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); + CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((1.0f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); + #endregion + + var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); + CreateBBCodeElement(workshopItem.Description, descriptionListBox); + + var showInSteamContainer + = new GUIFrame(new RectTransform((1.0f, 0.05f), verticalLayout.RectTransform), style: null); + CreateShowInSteamButton(workshopItem, new RectTransform((0.2f, 1.0f), showInSteamContainer.RectTransform, Anchor.CenterRight)); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs new file mode 100644 index 000000000..b67cab661 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -0,0 +1,422 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + private enum LobbyState + { + NotConnected, + Creating, + Owner, + Joining, + Joined + } + private static UInt64 lobbyID = 0; + private static LobbyState lobbyState = LobbyState.NotConnected; + private static Steamworks.Data.Lobby? currentLobby; + public static UInt64 CurrentLobbyID + { + get { return currentLobby?.Id ?? 0; } + } + + public static void CreateLobby(ServerSettings serverSettings) + { + if (lobbyState != LobbyState.NotConnected) { return; } + lobbyState = LobbyState.Creating; + TaskPool.Add("CreateLobbyAsync", Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10), + (lobby) => + { + if (lobbyState != LobbyState.Creating) + { + LeaveLobby(); + return; + } + + currentLobby = ((Task)lobby).Result; + + if (currentLobby == null) + { + DebugConsole.ThrowError("Failed to create Steam lobby"); + lobbyState = LobbyState.NotConnected; + return; + } + + DebugConsole.NewMessage("Lobby created!", Microsoft.Xna.Framework.Color.Lime); + + lobbyState = LobbyState.Owner; + lobbyID = (currentLobby?.Id).Value; + + if (serverSettings.IsPublic) + { + currentLobby?.SetPublic(); + } + else + { + currentLobby?.SetFriendsOnly(); + } + currentLobby?.SetJoinable(true); + + UpdateLobby(serverSettings); + }); + } + + public static void UpdateLobby(ServerSettings serverSettings) + { + if (GameMain.Client == null) + { + LeaveLobby(); + } + + if (lobbyState == LobbyState.NotConnected) + { + CreateLobby(serverSettings); + } + + if (lobbyState != LobbyState.Owner) + { + return; + } + + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); + + currentLobby?.SetData("name", serverSettings.ServerName); + currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); + currentLobby?.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); + //currentLobby?.SetData("hostipaddress", lobbyIP); + string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); + currentLobby?.SetData("pinglocation", pingLocation ?? ""); + currentLobby?.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID())); + currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString()); + + currentLobby?.SetData("message", serverSettings.ServerMessageText); + currentLobby?.SetData("version", GameMain.Version.ToString()); + + currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); + currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); + currentLobby?.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString()); + currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); + currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); + currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); + currentLobby?.SetData("allowspectating", serverSettings.AllowSpectating.ToString()); + currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); + currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); + currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); + currentLobby?.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); + currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); + currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); + currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); + + DebugConsole.Log("Lobby updated!"); + } + + public static void LeaveLobby() + { + if (lobbyState != LobbyState.NotConnected) + { + currentLobby?.Leave(); currentLobby = null; + lobbyState = LobbyState.NotConnected; + + lobbyID = 0; + + Steamworks.SteamMatchmaking.ResetActions(); + } + } + public static void JoinLobby(UInt64 id, bool joinServer) + { + if (currentLobby.HasValue && currentLobby.Value.Id == id) { return; } + if (lobbyID == id) { return; } + lobbyState = LobbyState.Joining; + lobbyID = id; + + TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), + (lobby) => + { + currentLobby = ((Task)lobby).Result; + lobbyState = LobbyState.Joined; + lobbyID = (currentLobby?.Id).Value; + if (joinServer) + { + GameMain.Instance.ConnectLobby = 0; + GameMain.Instance.ConnectName = currentLobby?.GetData("servername"); + GameMain.Instance.ConnectEndpoint = SteamIDUInt64ToString((currentLobby?.Owner.Id).Value); + } + }); + } + + public static bool GetServers(Action addToServerList, Action serverQueryFinished) + { + if (!IsInitialized) { return false; } + + int doneTasks = 0; + void taskDone() + { + doneTasks++; + if (doneTasks >= 2) + { + serverQueryFinished?.Invoke(); + serverQueryFinished = null; + } + } + + + Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => + { + DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); + }; + + TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(), + (t) => + { + Steamworks.Dispatch.OnDebugCallback = null; + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies"); + taskDone(); + return; + } + var lobbies = ((Task>)t).Result; + if (lobbies != null) + { + foreach (var lobby in lobbies) + { + if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } + + ServerInfo serverInfo = new ServerInfo(); + serverInfo.ServerName = lobby.GetData("name"); + serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); + serverInfo.LobbyID = lobby.Id; + bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword); + serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0; + serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1; + serverInfo.RespondedToSteamQuery = true; + + AssignLobbyDataToServerInfo(lobby, serverInfo); + + addToServerList(serverInfo); + } + } + taskDone(); + }); + + Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); + void onServer(Steamworks.Data.ServerInfo info, bool responsive) + { + if (string.IsNullOrEmpty(info.Name)) { return; } + + ServerInfo serverInfo = new ServerInfo + { + ServerName = info.Name, + HasPassword = info.Passworded, + IP = info.Address.ToString(), + Port = info.ConnectionPort.ToString(), + PlayerCount = info.Players, + MaxPlayers = info.MaxPlayers, + RespondedToSteamQuery = responsive + }; + + if (responsive) + { + TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(), + (t) => + { + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name); + return; + } + + var rules = ((Task>)t).Result; + AssignServerRulesToServerInfo(rules, serverInfo); + + CrossThread.RequestExecutionOnMainThread(() => + { + addToServerList(serverInfo); + }); + }); + } + else + { + CrossThread.RequestExecutionOnMainThread(() => + { + addToServerList(serverInfo); + }); + } + + } + serverQuery.OnResponsiveServer += (info) => onServer(info, true); + serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); + + TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(), + (t) => + { + serverQuery.Dispose(); + taskDone(); + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); + return; + } + }); + + return true; + } + + public static async Task> LobbyQueryRequest() + { + List allLobbies = new List(); + Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() + .FilterDistanceWorldwide() + .WithMaxResults(50); + //steamworks seems to unable to retrieve more than 50 + //lobbies per request; to work around this, we'll make + //up to 10 requests, asking to ignore all previous results + //in each subsequent request + for (int i = 0; i < 10; i++) + { + Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); + if (lobbies == null) { break; } + foreach (var l in lobbies) + { + lobbyQuery = lobbyQuery + .WithoutKeyValue("lobbyowner", l.GetData("lobbyowner")); + } + allLobbies.AddRange(lobbies); + } + + //make sure all returned lobbies are distinct, don't want any duplicates here + return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList(); + } + + public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo) + { + serverInfo.OwnerVerified = true; + + serverInfo.ServerMessage = lobby.GetData("message"); + serverInfo.GameVersion = lobby.GetData("version"); + + serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); + serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); + + string workshopIdData = lobby.GetData("contentpackageid"); + if (!string.IsNullOrEmpty(workshopIdData)) + { + serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData)); + } + else + { + string[] workshopUrls = lobby.GetData("contentpackageurl").Split(','); + serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); + } + + serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist"); + if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } + if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } + + serverInfo.AllowSpectating = getLobbyBool("allowspectating"); + serverInfo.AllowRespawn = getLobbyBool("allowrespawn"); + serverInfo.VoipEnabled = getLobbyBool("voicechatenabled"); + serverInfo.KarmaEnabled = getLobbyBool("karmaenabled"); + serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled"); + if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } + + serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; + serverInfo.GameMode = (lobby.GetData("gamemode") ?? "").ToIdentifier(); + if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; + + if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) + { + //invalid contentpackage info + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); + } + + string pingLocation = lobby.GetData("pinglocation"); + if (!string.IsNullOrEmpty(pingLocation)) + { + serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation); + } + + bool? getLobbyBool(string key) + { + string data = lobby.GetData(key); + if (string.IsNullOrEmpty(data)) { return null; } + return data == "True" || data == "true"; + } + } + + public static void AssignServerRulesToServerInfo(Dictionary rules, ServerInfo serverInfo) + { + serverInfo.OwnerVerified = true; + + if (rules == null) { return; } + + if (rules.ContainsKey("message")) serverInfo.ServerMessage = rules["message"]; + if (rules.ContainsKey("version")) serverInfo.GameVersion = rules["version"]; + + if (rules.ContainsKey("playercount")) + { + if (int.TryParse(rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; + } + + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); + if (rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); + if (rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); + if (rules.ContainsKey("contentpackageid")) + { + serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"])); + } + else if (rules.ContainsKey("contentpackageurl")) + { + string[] workshopUrls = rules["contentpackageurl"].Split(','); + serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); + } + + if (rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = rules["usingwhitelist"] == "True"; + if (rules.ContainsKey("modeselectionmode")) + { + if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode; + } + if (rules.ContainsKey("subselectionmode")) + { + if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode; + } + if (rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = rules["allowspectating"] == "True"; + if (rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = rules["allowrespawn"] == "True"; + if (rules.ContainsKey("voicechatenabled")) serverInfo.VoipEnabled = rules["voicechatenabled"] == "True"; + if (rules.ContainsKey("traitors")) + { + if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; + } + + if (rules.ContainsKey("gamestarted")) serverInfo.GameStarted = rules["gamestarted"] == "True"; + if (rules.ContainsKey("gamemode")) + { + serverInfo.GameMode = rules["gamemode"].ToIdentifier(); + } + if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle)) + { + serverInfo.PlayStyle = playStyle; + } + + if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) + { + //invalid contentpackage info + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs new file mode 100644 index 000000000..e34fabd34 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -0,0 +1,531 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Mime; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Directory = Barotrauma.IO.Directory; +using ItemOrPackage = Barotrauma.Either; +using Path = Barotrauma.IO.Path; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private class LocalThumbnail : IDisposable + { + public Texture2D? Texture { get; private set; } = null; + public bool Loading = true; + + public LocalThumbnail(string path) + { + TaskPool.Add($"LocalThumbnail {path}", + Task.Run(async () => + { + await Task.Yield(); + return TextureLoader.FromFile(path, compress: false, mipmap: false); + }), + (t) => + { + Loading = false; + Task texTask = (t as Task)!; + if (disposed) + { + texTask.Result?.Dispose(); + } + else + { + Texture = texTask.Result; + } + }); + } + + ~LocalThumbnail() + { + Dispose(); + } + + private bool disposed = false; + public void Dispose() + { + if (disposed) { return; } + + disposed = true; + Texture?.Dispose(); + } + } + + private LocalThumbnail? localThumbnail = null; + + private void CreateLocalThumbnail(string path, GUIFrame thumbnailContainer) + { + thumbnailContainer.ClearChildren(); + localThumbnail?.Dispose(); + localThumbnail = new LocalThumbnail(path); + CreateAsyncThumbnailComponent(thumbnailContainer, () => localThumbnail?.Texture, () => localThumbnail is { Loading: true }); + } + + private static async Task<(int FileCount, int ByteCount)> GetModDirInfo(string dir, GUITextBlock label) + { + int fileCount = 0; + int byteCount = 0; + + var files = Directory.GetFiles(dir, pattern: "*", option: System.IO.SearchOption.AllDirectories); + foreach (var file in files) + { + await Task.Yield(); + fileCount++; + byteCount += (int)(new Barotrauma.IO.FileInfo(file).Length); + label.Text = TextManager.GetWithVariables( + "ModDirInfo", + ("[filecount]", fileCount.ToString(CultureInfo.InvariantCulture)), + ("[size]", MathUtils.GetBytesReadable(byteCount))); + } + + return (fileCount, byteCount); + } + + private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) + { + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.WorkshopPackages.Refresh(); + + var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); + Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } + ? action + : null; + + void deselectItem() + { + deselectAction?.Invoke(); + SelectTab(Tab.Publish); + } + + parentFrame.ClearChildren(); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform), + childAnchor: Anchor.TopCenter); + + Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default; + ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package) + ? package + : ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackage? workshopPackage + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + if (localPackage is null) + { + new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null); + + //Local copy does not exist; check for Workshop copy + bool workshopCopyExists = + ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id); + + new GUITextBlock(new RectTransform((0.7f, 0.4f), mainLayout.RectTransform), + TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"), + wrap: true); + + var buttonLayout = new GUILayoutGroup(new RectTransform((0.6f, 0.1f), mainLayout.RectTransform), + isHorizontal: true); + var yesButton = new GUIButton(new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform), + text: TextManager.Get("Yes")) + { + OnClicked = (button, o) => + { + CoroutineManager.StartCoroutine(MessageBoxCoroutine((currentStepText, messageBox) + => CreateLocalCopy(currentStepText, workshopItem, parentFrame)), + $"CreateLocalCopy {workshopItem.Id}"); + return false; + } + }; + var noButton = new GUIButton(new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform), + text: TextManager.Get("No")) + { + OnClicked = (button, o) => + { + deselectItem(); + return false; + } + }; + } + else + { + if (!ContentPackageManager.LocalPackages.Contains(localPackage)) + { + throw new Exception($"Content package \"{localPackage.Name}\" is not a local package!"); + } + + var selectedTitle = + new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), workshopItem.Title ?? localPackage.Name, + font: GUIStyle.LargeFont); + if (workshopItem.Id != 0) + { + var showInSteamButton = CreateShowInSteamButton(workshopItem, new RectTransform((0.2f, 1.0f), selectedTitle.RectTransform, Anchor.CenterRight)); + } + + Spacer(mainLayout, height: 0.03f); + + var (leftTop, _, rightTop) + = CreateSidebars(mainLayout, leftWidth: 0.2f, centerWidth: 0.01f, rightWidth: 0.79f, + height: 0.4f); + leftTop.Stretch = true; + rightTop.Stretch = true; + + Label(leftTop, TextManager.Get("WorkshopItemPreviewImage"), GUIStyle.SubHeadingFont); + string? thumbnailPath = null; + var thumbnailContainer = CreateThumbnailContainer(leftTop, Vector2.One, ScaleBasis.BothWidth); + if (workshopItem.Id != 0) + { + CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer); + } + + var browseThumbnail = + new GUIButton(NewItemRectT(leftTop), + TextManager.Get("WorkshopItemBrowse"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + FileSelection.ClearFileTypeFilters(); + FileSelection.AddFileTypeFilter("PNG", "*.png"); + FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); + FileSelection.AddFileTypeFilter("All files", "*.*"); + FileSelection.SelectFileTypeFilter("*.png"); + FileSelection.CurrentDirectory + = Path.GetFullPath(Path.GetDirectoryName(localPackage.Path)!); + + FileSelection.OnFileSelected = (fn) => + { + thumbnailPath = fn; + CreateLocalThumbnail(thumbnailPath, thumbnailContainer); + }; + + FileSelection.Open = true; + + return false; + } + }; + + Label(rightTop, TextManager.Get("WorkshopItemTitle"), GUIStyle.SubHeadingFont); + var titleTextBox = new GUITextBox(NewItemRectT(rightTop), workshopItem.Title ?? localPackage.Name); + + Label(rightTop, TextManager.Get("WorkshopItemDescription"), GUIStyle.SubHeadingFont); + var descriptionTextBox + = ScrollableTextBox(rightTop, 6.0f, workshopItem.Description ?? string.Empty); + + var (leftBottom, _, rightBottom) + = CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f); + leftBottom.Stretch = true; + rightBottom.Stretch = true; + + Label(leftBottom, TextManager.Get("WorkshopItemVersion"), GUIStyle.SubHeadingFont); + var modVersion = localPackage.ModVersion; + if (workshopPackage is { ModVersion: { } workshopVersion } && + modVersion.Equals(workshopVersion, StringComparison.OrdinalIgnoreCase)) + { + modVersion = ModProject.IncrementModVersion(modVersion); + } + + char[] forbiddenVersionCharacters = { ';', '=' }; + var versionTextBox = new GUITextBox(NewItemRectT(leftBottom), modVersion); + versionTextBox.OnTextChanged += (box, text) => + { + if (text.Any(c => forbiddenVersionCharacters.Contains(c))) + { + foreach (var c in forbiddenVersionCharacters) + { + text = text.Replace($"{c}", ""); + } + + box.Text = text; + box.Flash(GUIStyle.Red); + } + + return true; + }; + + Label(leftBottom, TextManager.Get("WorkshopItemChangeNote"), GUIStyle.SubHeadingFont); + var changeNoteTextBox = ScrollableTextBox(leftBottom, 5.0f, ""); + + Label(rightBottom, TextManager.Get("WorkshopItemTags"), GUIStyle.SubHeadingFont); + var tagsList = CreateTagsList(SteamManager.Workshop.Tags, NewItemRectT(rightBottom, heightScale: 4.0f), + canBeFocused: true); + Dictionary tagButtons = tagsList.Content.Children.Cast() + .Select(b => ((Identifier)b.UserData, b)).ToDictionary(); + if (workshopItem.Tags != null) + { + foreach (Identifier tag in workshopItem.Tags.ToIdentifiers()) + { + if (tagButtons.TryGetValue(tag, out var button)) { button.Selected = true; } + } + } + + GUILayoutGroup visibilityLayout = new GUILayoutGroup(NewItemRectT(rightBottom), isHorizontal: true); + + var visibilityLabel = Label(visibilityLayout, TextManager.Get("WorkshopItemVisibility"), GUIStyle.SubHeadingFont); + visibilityLabel.RectTransform.RelativeSize = (0.6f, 1.0f); + visibilityLabel.TextAlignment = Alignment.CenterRight; + + Steamworks.Ugc.Visibility visibility = workshopItem.Visibility; + var visibilityDropdown = DropdownEnum( + visibilityLayout, + (v) => TextManager.Get($"WorkshopItemVisibility.{v}"), + visibility, + (v) => visibility = v); + visibilityDropdown.RectTransform.RelativeSize = (0.4f, 1.0f); + + var fileInfoLabel = Label(rightBottom, "", GUIStyle.Font, heightScale: 1.0f); + fileInfoLabel.TextAlignment = Alignment.CenterRight; + TaskPool.Add($"FileInfoLabel{workshopItem.Id}", GetModDirInfo(localPackage.Dir, fileInfoLabel), t => { }); + + GUILayoutGroup buttonLayout = new GUILayoutGroup(NewItemRectT(rightBottom), isHorizontal: true, childAnchor: Anchor.CenterRight); + + RectTransform newButtonRectT() + => new RectTransform((0.4f, 1.0f), buttonLayout.RectTransform); + + var publishItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemPublish")) + { + OnClicked = (button, o) => + { + //Reload the package to force hash recalculation + string packageName = localPackage.Name; + localPackage = ContentPackageManager.ReloadContentPackage(localPackage); + if (localPackage is null) + { + throw new Exception($"\"{packageName}\" was removed upon reload"); + } + + //Set up the Ugc.Editor object that we'll need to publish + Steamworks.Ugc.Editor ugcEditor = + workshopItem.Id == 0 + ? Steamworks.Ugc.Editor.NewCommunityFile + : new Steamworks.Ugc.Editor(workshopItem.Id); + ugcEditor = ugcEditor.WithTitle(titleTextBox.Text) + .WithDescription(descriptionTextBox.Text) + .WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value)) + .WithChangeLog(changeNoteTextBox.Text) + .WithMetaData($"gameversion={localPackage.GameVersion};modversion={versionTextBox.Text}") + .WithVisibility(visibility) + .WithPreviewFile(thumbnailPath); + + CoroutineManager.StartCoroutine( + MessageBoxCoroutine((currentStepText, messageBox) + => PublishItem(currentStepText, messageBox, versionTextBox.Text, ugcEditor, localPackage))); + + return false; + } + }; + + if (workshopItem.Id != 0) + { + var deleteItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemDelete"), color: GUIStyle.Red) + { + OnClicked = (button, o) => + { + var confirmDeletion = new GUIMessageBox( + headerText: TextManager.Get("WorkshopItemDelete"), + text: TextManager.GetWithVariable("WorkshopItemDeleteVerification", "[itemname]", workshopItem.Title!), + buttons: new[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmDeletion.Buttons[0].OnClicked = (yesBuffer, o1) => + { + TaskPool.Add($"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id), + t => + { + confirmDeletion.Close(); + deselectItem(); + }); + return false; + }; + confirmDeletion.Buttons[1].OnClicked = (noButton, o1) => + { + confirmDeletion.Close(); + return false; + }; + + return false; + }, + HoverColor = Color.Lerp(GUIStyle.Red, Color.White, 0.3f), + PressedColor = Color.Lerp(GUIStyle.Red, Color.Black, 0.3f), + }; + deleteItemButton.TextBlock.TextColor = Color.Black; + deleteItemButton.TextBlock.HoverTextColor = Color.Black; + } + } + } + + private IEnumerable MessageBoxCoroutine(Func> subcoroutine) + { + var messageBox = new GUIMessageBox("", "", relativeSize: (0.4f, 0.4f), buttons: new [] { TextManager.Get("Cancel") }); + messageBox.Buttons[0].OnClicked = (button, o) => + { + messageBox.Close(); + return false; + }; + + var currentStepText = new GUITextBlock(new RectTransform((1.0f, 0.8f), messageBox.InnerFrame.RectTransform), + "...", font: GUIStyle.Font) + { + CanBeFocused = false + }; + + foreach (var status in subcoroutine(currentStepText, messageBox)) + { + if (messageBox.Closed) + { + yield return CoroutineStatus.Success; + yield break; + } + else if (status == CoroutineStatus.Failure || status == CoroutineStatus.Success) + { + messageBox.Close(); + yield return status; + yield break; + } + else + { + yield return status; + } + } + } + + private IEnumerable CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) + { + ContentPackage? workshopCopy = + ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + if (workshopCopy is null) + { + if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) + { + //Must download! + while (!SteamManager.Workshop.CanBeInstalled(workshopItem)) + { + bool shouldForceInstall = workshopItem.IsInstalled + && Directory.Exists(workshopItem.Directory) + && !SteamManager.Workshop.IsItemDirectoryUpToDate(workshopItem); + shouldForceInstall |= workshopItem is + { IsDownloading: false, IsDownloadPending: false, IsInstalled: false }; + if (shouldForceInstall) + { + SteamManager.Workshop.ForceRedownload(workshopItem); + } + currentStepText.Text = $"Downloading {Percentage(workshopItem.DownloadAmount)}"; + yield return new WaitForSeconds(0.5f); + } + } + else + { + SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); + } + TaskPool.Add($"Install {workshopItem.Title}", + SteamManager.Workshop.WaitForInstall(workshopItem), + (t) => + { + ContentPackageManager.WorkshopPackages.Refresh(); + }); + while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + { + currentStepText.Text = $"Installing"; + yield return new WaitForSeconds(0.5f); + } + + workshopCopy = + ContentPackageManager.WorkshopPackages.First(p => p.SteamWorkshopId == workshopItem.Id); + } + + bool localCopyMade = false; + TaskPool.Add($"Create local copy {workshopItem.Title}", + SteamManager.Workshop.CreateLocalCopy(workshopCopy), + (t) => + { + ContentPackageManager.LocalPackages.Refresh(); + localCopyMade = true; + }); + while (!localCopyMade) + { + currentStepText.Text = $"Creating local copy"; + yield return new WaitForSeconds(0.5f); + } + + PopulatePublishTab(workshopItem, parentFrame); + + yield return CoroutineStatus.Success; + } + + private IEnumerable PublishItem( + GUITextBlock currentStepText, GUIMessageBox messageBox, + string modVersion, Steamworks.Ugc.Editor editor, ContentPackage localPackage) + { + bool stagingReady = false; + TaskPool.Add("CreatePublishStagingCopy", + SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage), + (t) => + { + Exception? exception = t.Exception?.InnerException ?? t.Exception; + if (exception != null) + { + throw new Exception($"Failed to create staging copy: {exception.Message} {exception.StackTrace}"); + } + stagingReady = true; + }); + currentStepText.Text = "Copying item to staging folder..."; + while (!stagingReady) { yield return new WaitForSeconds(0.5f); } + + editor = editor + .WithContent(SteamManager.Workshop.PublishStagingDir) + .ForAppId(SteamManager.AppID); + + messageBox.Buttons[0].Enabled = false; + Steamworks.Ugc.PublishResult? result = null; + TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})", + editor.SubmitAsync(), + (t) => + { + result = ((Task)t).Result; + }); + currentStepText.Text = "Submitting item to the Workshop..."; + while (!result.HasValue) { yield return new WaitForSeconds(0.5f); } + + if (result.Value.Success) + { + var resultId = result.Value.FileId; + Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId); + SteamManager.Workshop.ForceRedownload(resultItem); + while (!resultItem.IsInstalled) + { + currentStepText.Text = $"Downloading {Percentage(resultItem.DownloadAmount)}"; + yield return new WaitForSeconds(0.5f); + } + + bool installed = false; + TaskPool.Add( + "InstallNewlyPublished", + SteamManager.Workshop.WaitForInstall(resultItem), + (t) => + { + installed = true; + }); + while (!installed) + { + currentStepText.Text = $"Installing"; + yield return new WaitForSeconds(0.5f); + } + + var localModProject = new ModProject(localPackage) + { + SteamWorkshopId = resultId + }; + localModProject.Save(localPackage.Path); + ContentPackageManager.ReloadContentPackage(localPackage); + ContentPackageManager.WorkshopPackages.Refresh(); + + if (result.Value.NeedsWorkshopAgreement) + { + SteamManager.OverlayCustomURL(resultItem.Url); + } + } + SteamManager.Workshop.DeletePublishStagingCopy(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs new file mode 100644 index 000000000..e99288885 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -0,0 +1,143 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Networking; +using RestSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Color = Microsoft.Xna.Framework.Color; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + private static readonly List initializationErrors = new List(); + public static IReadOnlyList InitializationErrors => initializationErrors; + + private static void InitializeProjectSpecific() + { + if (IsInitialized) { return; } + + try + { + Steamworks.SteamClient.Init(AppID, false); + IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid; + + if (IsInitialized) + { + DebugConsole.NewMessage( + $"Logged in as {GetUsername()} (SteamID {SteamIDUInt64ToString(GetSteamID())})"); + + popularTags.Clear(); + int i = 0; + foreach (KeyValuePair commonness in tagCommonness) + { + popularTags.Insert(i, commonness.Key); + i++; + } + } + + Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking; + } + catch (DllNotFoundException) + { + IsInitialized = false; + initializationErrors.Add("SteamDllNotFound".ToIdentifier()); + } + catch (Exception e) + { + DebugConsole.ThrowError("SteamManager initialization threw an exception", e); + IsInitialized = false; + initializationErrors.Add("SteamClientInitFailed".ToIdentifier()); + } + + if (!IsInitialized) + { + try + { + if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } + } + catch (Exception e) + { + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError("Disposing Steam client failed.", e); + } + } + else + { + //Steamworks is completely insane so the following needs comments: + + //This callback seems to take place when the item in question has not been downloaded recently + Steamworks.SteamUGC.GlobalOnItemInstalled = id => Workshop.OnItemDownloadComplete(id); + + //This callback seems to take place when the item has been downloaded recently and an update + //or a redownload has taken place + Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => Workshop.OnItemDownloadComplete(id); + + //Maybe I'm completely wrong! All I know is that we need to handle both! + } + } + + public static bool NetworkingDebugLog { get; private set; } = false; + + private static void LogSteamworksNetworking(Steamworks.NetDebugOutput nType, string pszMsg) + { + DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange); + } + + public static void SetSteamworksNetworkingDebugLog(bool enabled) + { + if (enabled == NetworkingDebugLog) { return; } + if (enabled) + { + Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.Everything; + } + else + { + Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.None; + } + NetworkingDebugLog = enabled; + } + + public static async Task InitRelayNetworkAccess() + { + if (!IsInitialized) { return; } + + await Task.Yield(); + Steamworks.SteamNetworkingUtils.InitRelayNetworkAccess(); + + //SetSteamworksNetworkingDebugLog(true); + var status = Steamworks.SteamNetworkingUtils.Status; + while (status.Avail != Steamworks.SteamNetworkingAvailability.Current) + { + if (status.Avail == Steamworks.SteamNetworkingAvailability.CannotTry || + status.Avail == Steamworks.SteamNetworkingAvailability.Previously || + status.Avail == Steamworks.SteamNetworkingAvailability.Failed) + { + DebugConsole.ThrowError($"Failed to initialize Steamworks network relay: " + + $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + + $"{Steamworks.SteamNetworkingUtils.Status.AvailNetConfig}, " + + $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + + $"{Steamworks.SteamNetworkingUtils.Status.Msg}"); + break; + } + await Task.Delay(25); + status = Steamworks.SteamNetworkingUtils.Status; + } + //SetSteamworksNetworkingDebugLog(false); + } + + + public static bool OverlayCustomURL(string url) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) + { + return false; + } + + Steamworks.SteamFriends.OpenWebOverlay(url); + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs new file mode 100644 index 000000000..75372311c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs @@ -0,0 +1,109 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private static RectTransform NewItemRectT(GUILayoutGroup parent, float heightScale = 1.0f) + => new RectTransform((1.0f, 0.06f * heightScale), parent.RectTransform, Anchor.CenterLeft); + + private static void Spacer(GUILayoutGroup parent, float height = 0.03f) + { + new GUIFrame(new RectTransform((1.0f, height), parent.RectTransform, Anchor.CenterLeft), style: null); + } + + private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font, float heightScale = 1.0f) + { + return new GUITextBlock(NewItemRectT(parent, heightScale), str, font: font); + } + + private static GUITextBox ScrollableTextBox(GUILayoutGroup parent, float heightScale, string text) + { + var containingListBox = new GUIListBox(NewItemRectT(parent, heightScale)); + var textBox = new GUITextBox( + new RectTransform(Vector2.One, containingListBox.Content.RectTransform), + "", style: "GUITextBoxNoBorder", wrap: true, + textAlignment: Alignment.TopLeft); + textBox.OnTextChanged += (textBox, text) => + { + string wrappedText = textBox.TextBlock.WrappedText.Value; + int measuredHeight = (int)textBox.Font.MeasureString(wrappedText).Y; + textBox.RectTransform.NonScaledSize = + (containingListBox.Content.Rect.Width, + Math.Max(measuredHeight, containingListBox.Content.Rect.Height)); + containingListBox.UpdateScrollBarSize(); + + return true; + }; + textBox.OnEnterPressed += (textBox, text) => + { + string str = textBox.Text; + int cursorPos = textBox.CaretIndex; + textBox.Text = $"{str[..cursorPos]}\n{str[cursorPos..]}"; + textBox.CaretIndex = cursorPos + 1; + + return true; + }; + textBox.Text = text; + return textBox; + } + + private static GUIDropDown DropdownEnum( + GUILayoutGroup parent, Func textFunc, T currentValue, + Action setter) where T : Enum + => Dropdown(parent, textFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); + + private static GUIDropDown Dropdown( + GUILayoutGroup parent, Func textFunc, IReadOnlyList values, T currentValue, + Action setter, float heightScale = 1.0f) + { + var dropdown = new GUIDropDown(NewItemRectT(parent, heightScale)); + SwapDropdownValues(dropdown, textFunc, values, currentValue, setter); + return dropdown; + } + + private static void SwapDropdownValues( + GUIDropDown dropdown, Func textFunc, IReadOnlyList values, T currentValue, + Action setter) + { + if (dropdown.ListBox.Content.Children.Any(c => !(c.UserData is T))) + { + throw new Exception("SwapValues must preserve the type of the dropdown's userdata"); + } + + dropdown.OnSelected = null; + dropdown.ClearChildren(); + + values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v)); + dropdown.Select(values.IndexOf(currentValue)); + dropdown.OnSelected = (dd, obj) => + { + setter((T)obj); + return true; + }; + } + + private static int Round(float v) => (int)MathF.Round(v); + private static string Percentage(float v) => $"{Round(v * 100)}%"; + + private struct ActionCarrier + { + public readonly Identifier Id; + public readonly Action Action; + public ActionCarrier(Identifier id, Action action) + { + Id = id; + Action = action; + } + } + + private GUIComponent CreateActionCarrier(GUIComponent parent, Identifier id, Action action) + => new GUIFrame(new RectTransform(Vector2.Zero, parent.RectTransform), style: null) + { UserData = new ActionCarrier(id, action) }; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs new file mode 100644 index 000000000..25d4144aa --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -0,0 +1,285 @@ +#nullable enable +using Microsoft.Xna.Framework.Graphics; +using RestSharp; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Barotrauma.IO; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + public static partial class Workshop + { + public static readonly ImmutableArray Tags = new [] + { + "submarine", + "item", + "monster", + "art", + "mission", + "event set", + "total conversion", + "environment", + "item assembly", + "language", + }.ToIdentifiers().ToImmutableArray(); + + public class ItemThumbnail : IDisposable + { + private struct RefCounter + { + internal bool Loading; + internal Texture2D? Texture; + internal int Count; + } + private readonly static Dictionary TextureRefs + = new Dictionary(); + + public UInt64 ItemId { get; private set; } + public Texture2D? Texture + { + get + { + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(ItemId, out var refCounter)) + { + return refCounter.Texture; + } + } + return null; + } + } + + public bool Loading + { + get + { + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(ItemId, out var refCounter)) + { + return refCounter.Loading; + } + } + return false; + } + } + + public ItemThumbnail(in Steamworks.Ugc.Item item, CancellationToken cancellationToken) + { + ItemId = item.Id; + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(ItemId, out var refCounter)) + { + TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count + 1, Loading = refCounter.Loading }; + } + else + { + TextureRefs[ItemId] = new RefCounter { Texture = null, Count = 1, Loading = true }; + TaskPool.Add($"Workshop thumbnail {item.Title}", GetTexture(item, cancellationToken), SaveTextureToRefCounter(item.Id)); + } + } + } + + ~ItemThumbnail() + { + Dispose(); + } + + public void Dispose() + { + if (ItemId == 0) { return; } + lock (TextureRefs) + { + var refCounter = TextureRefs[ItemId]; + TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count - 1 }; + if (TextureRefs[ItemId].Count <= 0) + { + TextureRefs[ItemId].Texture?.Dispose(); + TextureRefs.Remove(ItemId); + } + ItemId = 0; + } + } + + private static async Task GetTexture(Steamworks.Ugc.Item item, CancellationToken cancellationToken) + { + await Task.Yield(); + + string thumbnailUrl = item.PreviewImageUrl; + if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } + var client = new RestClient(thumbnailUrl); + var request = new RestRequest(".", Method.GET); + IRestResponse response = await client.ExecuteTaskAsync(request, cancellationToken); + if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) + { + using var dataStream = new System.IO.MemoryStream(); + await dataStream.WriteAsync(response.RawBytes, cancellationToken); + dataStream.Seek(0, System.IO.SeekOrigin.Begin); + return TextureLoader.FromStream(dataStream, compress: false); + } + return null; + } + + private static Action SaveTextureToRefCounter(UInt64 itemId) + => (t) => + { + if (t.IsCanceled) { return; } + Texture2D? texture = ((Task)t).Result; + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(itemId, out var refCounter)) + { + TextureRefs[itemId] = new RefCounter { Texture = texture, Count = refCounter.Count, Loading = false }; + } + else if (texture != null) + { + texture.Dispose(); + } + } + }; + + public override int GetHashCode() => (int)ItemId; + + public override bool Equals(object? obj) + => obj is ItemThumbnail { ItemId: UInt64 otherId } + && otherId == ItemId; + } + + public const string PublishStagingDir = "WorkshopStaging"; + + public static void DeletePublishStagingCopy() + { + if (Directory.Exists(PublishStagingDir)) { Directory.Delete(PublishStagingDir, recursive: true); } + } + + private static void RefreshLocalMods() + { + CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh()); + } + + public static async Task CreatePublishStagingCopy(string modVersion, ContentPackage contentPackage) + { + await Task.Yield(); + + if (!ContentPackageManager.LocalPackages.Contains(contentPackage)) + { + throw new Exception("Expected local package"); + } + + DeletePublishStagingCopy(); + Directory.CreateDirectory(PublishStagingDir); + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir); + + //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be + ModProject modProject = new ModProject(contentPackage); + modProject.ModVersion = modVersion; + modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName)); + } + + public static async Task CreateLocalCopy(ContentPackage contentPackage) + { + await Task.Yield(); + + if (!ContentPackageManager.WorkshopPackages.Contains(contentPackage)) + { + throw new Exception("Expected Workshop package"); + } + + if (contentPackage.SteamWorkshopId == 0) + { + throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}"); + } + + string sanitizedName = ToolBox.RemoveInvalidFileNameChars(contentPackage.Name).Trim(); + if (sanitizedName.IsNullOrWhiteSpace()) + { + throw new Exception($"Sanitized name for {contentPackage.Name} is empty"); + } + + string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; + if (File.Exists(newPath) || Directory.Exists(newPath)) + { + throw new Exception($"{newPath} already exists"); + } + + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath); + + ModProject modProject = new ModProject(contentPackage); + modProject.DiscardHashAndInstallTime(); + modProject.Save(Path.Combine(newPath, ContentPackage.FileListFileName)); + + RefreshLocalMods(); + + return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == contentPackage.SteamWorkshopId); + } + + private struct InstallWaiter + { + private static readonly HashSet waitingIds = new HashSet(); + public ulong Id { get; private set; } + + public InstallWaiter(ulong id) + { + Id = id; + lock (waitingIds) { waitingIds.Add(Id); } + } + + public bool Waiting + { + get + { + if (Id == 0) { return false; } + + lock (waitingIds) + { + return waitingIds.Contains(Id); + } + } + } + + public static void StopWaiting(ulong id) + { + lock (waitingIds) + { + waitingIds.Remove(id); + } + } + } + + public static async Task WaitForInstall(Steamworks.Ugc.Item item) + => await WaitForInstall(item.Id); + + public static async Task WaitForInstall(ulong item) + { + var installWaiter = new InstallWaiter(item); + while (installWaiter.Waiting) { await Task.Delay(500); } + } + + public static void OnItemDownloadComplete(ulong id, bool forceInstall = false) + { + if (!(Screen.Selected is MainMenuScreen) && !forceInstall) + { + if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id)) + { + MainMenuScreen.WorkshopItemsToUpdate.Enqueue(id); + } + return; + } + else if (CanBeInstalled(id) + && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id)) + { + TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs new file mode 100644 index 000000000..c921a71aa --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs @@ -0,0 +1,442 @@ +#nullable enable +using System; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Threading; +using System.Xml.Linq; +using Barotrauma.IO; +using Microsoft.Xna.Framework.Graphics; +using ItemOrPackage = Barotrauma.Either; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + public enum Tab + { + InstalledMods, + //Overrides, //TODO: implement later + PopularMods, + Publish + } + + private GUILayoutGroup tabber; + private Dictionary tabContents; + + private GUIFrame contentFrame; + + private CorePackage enabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); + + private readonly GUIDropDown enabledCoreDropdown; + private readonly GUIListBox enabledRegularModsList; + private readonly GUIListBox disabledRegularModsList; + private readonly Action onInstalledInfoButtonHit; + + private CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); + private readonly HashSet itemThumbnails = new HashSet(); + + private readonly GUIListBox popularModsList; + private readonly GUIListBox selfModsList; + + public WorkshopMenu(GUIFrame parent) + { + var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false); + + tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; + tabContents = new Dictionary(); + + contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); + + CreateInstalledModsTab(out enabledCoreDropdown, out enabledRegularModsList, out disabledRegularModsList, out onInstalledInfoButtonHit); + CreatePopularModsTab(out popularModsList); + CreatePublishTab(out selfModsList); + + SelectTab(Tab.InstalledMods); + } + + private void SwitchContent(GUIFrame newContent) + { + contentFrame.Children.ForEach(c => c.Visible = false); + newContent.Visible = true; + } + + public void SelectTab(Tab tab) + { + SwitchContent(tabContents[tab].Content); + tabber.Children.ForEach(c => + { + if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; } + }); + if (!taskCancelSrc.IsCancellationRequested) { taskCancelSrc.Cancel(); } + itemThumbnails.ForEach(t => t.Dispose()); + itemThumbnails.Clear(); + switch (tab) + { + case Tab.InstalledMods: + PopulateInstalledModLists(); + break; + case Tab.PopularMods: + PopulateItemList(popularModsList, SteamManager.Workshop.GetPopularItems(), includeSubscribeButton: true); + break; + case Tab.Publish: + PopulateItemList(selfModsList, SteamManager.Workshop.GetPublishedItems(), includeSubscribeButton: false, onFill: AddUnpublishedMods); + break; + } + } + + private void AddButtonToTabber(Tab tab, GUIFrame content) + { + var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get($"workshopmenutab.{tab}"), style: "GUITabButton") + { + OnClicked = (b, _) => + { + SelectTab(tab); + return false; + } + }; + button.RectTransform.MaxSize = RectTransform.MaxPoint; + button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + + tabContents.Add(tab, (button, content)); + } + + private GUIFrame CreateNewContentFrame(Tab tab) + { + var content = new GUIFrame(new RectTransform(Vector2.One * 0.98f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null); + AddButtonToTabber(tab, content); + return content; + } + + private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars( + GUIComponent parent, + float leftWidth = 0.3875f, + float centerWidth = 0.025f, + float rightWidth = 0.5875f, + bool split = false, + float height = 1.0f) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false); + var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform), + onDraw: (sb, c) => + { + sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); + }); + } + GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, center, right); + } + + private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) + { + if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) + { + //move the dragged elements to the index determined previously + var draggedElement = from.DraggedElement; + + var selected = from.AllSelected.ToList(); + selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); + + float oldCount = to.Content.CountChildren; + float newCount = oldCount + selected.Count; + + var offset = draggedElement.RectTransform.AbsoluteOffset; + offset += from.Content.Rect.Location; + offset -= to.Content.Rect.Location; + + for (int i = 0; i < selected.Count; i++) + { + var c = selected[i]; + c.Parent.RemoveChild(c); + c.RectTransform.Parent = to.Content.RectTransform; + c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); + } + + from.DraggedElement = null; + from.Deselect(); + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + + //recalculate the dragged element's offset so it doesn't jump around + draggedElement.RectTransform.AbsoluteOffset = offset; + + to.DraggedElement = draggedElement; + + to.BarScroll = to.BarScroll * (oldCount / newCount); + } + } + + private void CreateInstalledModsTab( + out GUIDropDown enabledCoreDropdown, + out GUIListBox enabledRegularModsList, + out GUIListBox disabledRegularModsList, + out Action onInstalledInfoButtonHit) + { + GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); + + CreateWorkshopItemDetailContainer( + content, + out var outerContainer, + onSelected: (itemOrPackage, selectedFrame) => + { + if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } + }, + onDeselected: PopulateInstalledModLists, + out onInstalledInfoButtonHit, out var deselect); + + GUILayoutGroup mainLayout = + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); + mainLayout.RectTransform.SetAsFirstChild(); + GUILayoutGroup coreSelectionLayout = + new GUILayoutGroup(new RectTransform((0.5f, 0.15f), mainLayout.RectTransform)); + Label(coreSelectionLayout, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f / 0.15f); + enabledCoreDropdown = Dropdown(coreSelectionLayout, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }, + heightScale: 1.0f / 0.15f); + + var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.78f); + right.ChildAnchor = Anchor.TopRight; + + Action swapFunc(GUIListBox from, GUIListBox to) + { + return () => + { + to.Deselect(); + var selected = from.AllSelected.ToArray(); + foreach (var frame in selected) + { + frame.Parent.RemoveChild(frame); + frame.RectTransform.Parent = to.Content.RectTransform; + } + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + }; + } + + Action? currentCenterCallback = null; + + //enabled mods + Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.92f), left.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true + }; + enabledRegularModsList = enabledModsList; + + //disabled mods + Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); + var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.92f), right.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true + }; + disabledRegularModsList = disabledModsList; + + var centerButton = + new GUIButton( + new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth, + anchor: Anchor.Center), + style: "GUIButtonToggleLeft") + { + Visible = false, + OnClicked = (button, o) => + { + currentCenterCallback?.Invoke(); + return false; + } + }; + + enabledModsList.OnSelected = (frame, o) => + { + disabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); + + currentCenterCallback = swapFunc(enabledModsList, disabledModsList); + + return true; + }; + disabledModsList.OnSelected = (frame, o) => + { + enabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); + + currentCenterCallback = swapFunc(disabledModsList, enabledModsList); + + return true; + }; + + var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); + searchRectT.RelativeSize = (0.5f, searchRectT.RelativeSize.Y); + var searchHolder = new GUIFrame(searchRectT, style: null); + var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), ""); + var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, + textColor: Color.DarkGray * 0.6f, + text: TextManager.Get("Search") + "...", + textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = searchBox.Text.IsNullOrWhiteSpace(); }; + + searchBox.OnTextChanged += (sender, str) => + { + enabledModsList.Content.Children.Concat(disabledModsList.Content.Children) + .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() + || (c.UserData is ContentPackage p + && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + return true; + }; + + new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + HandleDraggingAcrossModLists(enabledModsList, disabledModsList); + HandleDraggingAcrossModLists(disabledModsList, enabledModsList); + }, + onDraw: (spriteBatch, component) => + { + enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + }); + } + + private void PopulateInstalledModLists() + { + ContentPackageManager.UpdateContentPackageList(); + + SwapDropdownValues(enabledCoreDropdown, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }); + + void addRegularModToList(RegularPackage mod, GUIListBox list) + { + var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), + style: "ListBoxElement") + { + UserData = mod + }; + + var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var dragIndicator = new GUIButton(new RectTransform((0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIDragIndicator") + { + CanBeFocused = false + }; + + var modNameScissor + = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)); + var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), text: mod.Name); + if (ContentPackageManager.LocalPackages.Contains(mod)) + { + var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: "WorkshopMenu.EditButton") + { + OnClicked = (button, o) => + { + ToolBox.OpenFileWithShell(mod.Dir); + return false; + } + }; + } + else if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + var infoButton = new GUIButton( + new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: "WorkshopMenu.InfoButton") + { + OnClicked = (button, o) => + { + TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } + if (item is null) { return; } + onInstalledInfoButtonHit(item.Value); + }); + return false; + } + }; + TaskPool.Add( + $"DetermineUpdateRequired{mod.SteamWorkshopId}", + mod.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + if (!isUpToDate) + { + infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + } + }); + } + } + + enabledRegularModsList.ClearChildren(); + for (int i = 0; i < ContentPackageManager.EnabledPackages.Regular.Count; i++) + { + var mod = ContentPackageManager.EnabledPackages.Regular[i]; + addRegularModToList(mod, enabledRegularModsList); + } + + disabledRegularModsList.ClearChildren(); + foreach (var mod in ContentPackageManager.RegularPackages) + { + if (ContentPackageManager.EnabledPackages.Regular.Contains(mod)) { continue; } + addRegularModToList(mod, disabledRegularModsList); + } + } + + private void CreatePopularModsTab(out GUIListBox popularModsList) + { + GUIFrame content = CreateNewContentFrame(Tab.PopularMods); + + CreateWorkshopItemList(content, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo); + } + + private void CreatePublishTab(out GUIListBox selfModsList) + { + GUIFrame content = CreateNewContentFrame(Tab.Publish); + + CreateWorkshopItemOrPackageList(content, out _, out selfModsList, onSelected: PopulatePublishTab); + } + + public void Apply() + { + ContentPackageManager.EnabledPackages.SetCore(enabledCorePackage); + ContentPackageManager.EnabledPackages.SetRegular(enabledRegularModsList.Content.Children + .Where(c => c.UserData is RegularPackage).Select(c => (RegularPackage)c.UserData).ToArray()); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index c4c40edb9..3b9f2241e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -27,7 +27,7 @@ namespace Barotrauma internal abstract partial class Command { - public abstract string GetDescription(); + public abstract LocalizedString GetDescription(); } /// @@ -88,7 +88,7 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { if (Resized) { @@ -303,7 +303,7 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { if (WasDeleted) { @@ -364,7 +364,7 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { if (wasDropped) { @@ -379,8 +379,8 @@ namespace Barotrauma } return Receivers.Count > 1 - ? TextManager.GetWithVariables("Undo.ContainedItemsMultiple", new[] { "[count]", "[container]" }, new[] { Receivers.Count.ToString(), container }) - : TextManager.GetWithVariables("Undo.ContainedItem", new[] { "[item]", "[container]" }, new[] { Receivers.FirstOrDefault().Item.Name, container }); + ? TextManager.GetWithVariables("Undo.ContainedItemsMultiple", ("[count]", Receivers.Count.ToString()), ("[container]", container)) + : TextManager.GetWithVariables("Undo.ContainedItem", ("[item]", Receivers.FirstOrDefault().Item.Name), ("[container]", container)); } } @@ -391,7 +391,7 @@ namespace Barotrauma { private Dictionary> OldProperties; private readonly List Receivers; - private readonly string PropertyName; + private readonly Identifier PropertyName; private readonly object NewProperties; private string sanitizedProperty; @@ -404,7 +404,7 @@ namespace Barotrauma /// Real property name, not all lowercase /// /// - public PropertyCommand(List receivers, string propertyName, object newData, Dictionary> oldData) + public PropertyCommand(List receivers, Identifier propertyName, object newData, Dictionary> oldData) { Receivers = receivers; PropertyName = propertyName; @@ -414,7 +414,7 @@ namespace Barotrauma SanitizeProperty(); } - public PropertyCommand(ISerializableEntity receiver, string propertyName, object newData, object oldData) + public PropertyCommand(ISerializableEntity receiver, Identifier propertyName, object newData, object oldData) { Receivers = new List { receiver }; PropertyName = propertyName; @@ -485,9 +485,9 @@ namespace Barotrauma if (receiver.SerializableProperties != null) { - Dictionary props = receiver.SerializableProperties; + Dictionary props = receiver.SerializableProperties; - if (props.TryGetValue(PropertyName.ToLowerInvariant(), out SerializableProperty prop)) + if (props.TryGetValue(PropertyName, out SerializableProperty prop)) { prop.TrySetValue(receiver, data); // Update the editing hud @@ -512,11 +512,17 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { return Receivers.Count > 1 - ? TextManager.GetWithVariables("Undo.ChangedPropertyMultiple", new[] { "[property]", "[count]", "[value]" }, new[] { PropertyName, Receivers.Count.ToString(), sanitizedProperty }) - : TextManager.GetWithVariables("Undo.ChangedProperty", new[] { "[property]", "[item]", "[value]" }, new[] { PropertyName, Receivers.FirstOrDefault()?.Name, sanitizedProperty }); + ? TextManager.GetWithVariables("Undo.ChangedPropertyMultiple", + ("[property]", PropertyName.Value), + ("[count]", Receivers.Count.ToString()), + ("[value]", sanitizedProperty)) + : TextManager.GetWithVariables("Undo.ChangedProperty", + ("[property]", PropertyName.Value), + ("[item]", Receivers.FirstOrDefault()?.Name), + ("[value]", sanitizedProperty)); } } @@ -560,7 +566,7 @@ namespace Barotrauma public override void Cleanup() { } - public override string GetDescription() + public override LocalizedString GetDescription() { return TextManager.GetWithVariable("Undo.MovedItem", "[item]", targetItem.Name); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs new file mode 100644 index 000000000..8e652c5cd --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs @@ -0,0 +1,34 @@ +#nullable enable +namespace Barotrauma +{ + public class LimitLString : LocalizedString + { + private readonly LocalizedString nestedStr; + private readonly GUIFont font; + private readonly int maxWidth; + + private ScalableFont? cachedFont = null; + private uint cachedFontSize = 0; + + public LimitLString(LocalizedString text, GUIFont font, int maxWidth) + { + this.nestedStr = text; + this.font = font; + this.maxWidth = maxWidth; + } + + public override bool Loaded => nestedStr.Loaded; + protected override bool MustRetrieveValue() + { + return base.MustRetrieveValue() || cachedFont != font.Value || cachedFont.Size != font.Size; + } + + public override void RetrieveValue() + { + cachedValue = ToolBox.LimitString(nestedStr.Value, font.Value, maxWidth); + cachedFont = font.Value; + cachedFontSize = font.Size; + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs new file mode 100644 index 000000000..f13fdd117 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace Barotrauma +{ + public class WrappedLString : LocalizedString + { + private readonly LocalizedString nestedStr; + private readonly float lineLength; + private readonly GUIFont font; + private readonly float textScale; + + public WrappedLString(LocalizedString text, float lineLength, GUIFont font, float textScale = 1.0f) + { + this.nestedStr = text; + this.lineLength = lineLength; + this.font = font; + this.textScale = textScale; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.Value, textScale); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs index deed55665..c427adf4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs @@ -6,35 +6,16 @@ using System.Xml.Linq; namespace Barotrauma { - class TraitorMissionPrefab + class TraitorMissionPrefab : Prefab { - public static readonly List List = new List(); - - public readonly string Identifier; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public readonly Sprite Icon; public readonly Color IconColor; - public static void Init() + public TraitorMissionPrefab(ContentXElement element, TraitorMissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) { - List.Clear(); - var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) { continue; } - - foreach (XElement element in doc.Root.Elements()) - { - List.Add(new TraitorMissionPrefab(element)); - } - } - } - - private TraitorMissionPrefab(XElement element) - { - Identifier = element.GetAttributeString("identifier", ""); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { @@ -43,5 +24,10 @@ namespace Barotrauma } } } + + public override void Dispose() + { + Icon?.Remove(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs index 8e2c5e73d..eaea08977 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs @@ -7,7 +7,7 @@ namespace Barotrauma { public TraitorMissionResult(IReadMessage inc) { - MissionIdentifier = inc.ReadString(); + MissionIdentifier = inc.ReadIdentifier(); EndMessage = inc.ReadString(); Success = inc.ReadBoolean(); byte characterCount = inc.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs index 7f2243b45..7443204bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class UpgradePrefab { - public readonly List DecorativeSprites = new List(); + public readonly ImmutableArray DecorativeSprites = new ImmutableArray(); public Sprite Sprite { get; private set; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs index f6c015be6..3f0b622bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs @@ -36,26 +36,15 @@ using System.Configuration; using System.Globalization; using Barotrauma.IO; using System.Text; -#if NET_4_0 -using System.Web.Configuration; -#endif namespace RestSharp.Contrib { -#if NET_4_0 - public -#endif class HttpEncoder { static char[] hexChars = "0123456789abcdef".ToCharArray(); static object entitiesLock = new object(); static SortedDictionary entities; -#if NET_4_0 - static Lazy defaultEncoder; - static Lazy currentEncoderLazy; -#else static HttpEncoder defaultEncoder; -#endif static HttpEncoder currentEncoder; static IDictionary Entities @@ -76,53 +65,29 @@ namespace RestSharp.Contrib { get { -#if NET_4_0 - if (currentEncoder == null) - currentEncoder = currentEncoderLazy.Value; -#endif return currentEncoder; } -#if NET_4_0 - set { - if (value == null) - throw new ArgumentNullException ("value"); - currentEncoder = value; - } -#endif } public static HttpEncoder Default { get { -#if NET_4_0 - return defaultEncoder.Value; -#else return defaultEncoder; -#endif } } static HttpEncoder() { -#if NET_4_0 - defaultEncoder = new Lazy (() => new HttpEncoder ()); - currentEncoderLazy = new Lazy (new Func (GetCustomEncoderFromConfig)); -#else defaultEncoder = new HttpEncoder(); currentEncoder = defaultEncoder; -#endif } public HttpEncoder() { } -#if NET_4_0 - protected internal virtual -#else - internal static -#endif - void HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue) + + internal static void HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue) { if (String.IsNullOrEmpty(headerName)) encodedHeaderName = headerName; @@ -161,66 +126,8 @@ namespace RestSharp.Contrib return input; } -#if NET_4_0 - protected internal virtual void HtmlAttributeEncode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - if (String.IsNullOrEmpty (value)) - return; - - output.Write (HtmlAttributeEncode (value)); - } - - protected internal virtual void HtmlDecode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlDecode (value)); - } - - protected internal virtual void HtmlEncode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlEncode (value)); - } - - protected internal virtual byte[] UrlEncode (byte[] bytes, int offset, int count) - { - return UrlEncodeToBytes (bytes, offset, count); - } - - static HttpEncoder GetCustomEncoderFromConfig () - { - var cfg = WebConfigurationManager.GetSection ("system.web/httpRuntime") as HttpRuntimeSection; - string typeName = cfg.EncoderType; - - if (String.Compare (typeName, "System.Web.Util.HttpEncoder", StringComparison.OrdinalIgnoreCase) == 0) - return Default; - - Type t = Type.GetType (typeName, false); - if (t == null) - throw new ConfigurationErrorsException (String.Format ("Could not load type '{0}'.", typeName)); - - if (!typeof (HttpEncoder).IsAssignableFrom (t)) - throw new ConfigurationErrorsException ( - String.Format ("'{0}' is not allowed here because it does not extend class 'System.Web.Util.HttpEncoder'.", typeName) - ); - - return Activator.CreateInstance (t, false) as HttpEncoder; - } -#endif -#if NET_4_0 - protected internal virtual -#else - internal static -#endif - string UrlPathEncode(string value) + internal static string UrlPathEncode(string value) { if (String.IsNullOrEmpty(value)) return value; @@ -240,7 +147,7 @@ namespace RestSharp.Contrib int blen = bytes.Length; if (blen == 0) - return new byte[0]; + return Array.Empty(); if (offset < 0 || offset >= blen) throw new ArgumentOutOfRangeException("offset"); @@ -268,11 +175,7 @@ namespace RestSharp.Contrib for (int i = 0; i < s.Length; i++) { char c = s[i]; - if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159 -#if NET_4_0 - || c == '\'' -#endif -) + if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159) { needEncode = true; break; @@ -302,11 +205,6 @@ namespace RestSharp.Contrib case '"': output.Append("""); break; -#if NET_4_0 - case '\'': - output.Append ("'"); - break; -#endif case '\uff1c': output.Append("<"); break; @@ -334,25 +232,17 @@ namespace RestSharp.Contrib internal static string HtmlAttributeEncode(string s) { -#if NET_4_0 - if (String.IsNullOrEmpty (s)) - return String.Empty; -#else if (s == null) return null; if (s.Length == 0) return String.Empty; -#endif + bool needEncode = false; for (int i = 0; i < s.Length; i++) { char c = s[i]; - if (c == '&' || c == '"' || c == '<' -#if NET_4_0 - || c == '\'' -#endif -) + if (c == '&' || c == '"' || c == '<') { needEncode = true; break; @@ -376,11 +266,6 @@ namespace RestSharp.Contrib case '<': output.Append("<"); break; -#if NET_4_0 - case '\'': - output.Append ("'"); - break; -#endif default: output.Append(s[i]); break; @@ -399,9 +284,7 @@ namespace RestSharp.Contrib if (s.IndexOf('&') == -1) return s; -#if NET_4_0 - StringBuilder rawEntity = new StringBuilder (); -#endif + StringBuilder entity = new StringBuilder(); StringBuilder output = new StringBuilder(); int len = s.Length; @@ -422,9 +305,6 @@ namespace RestSharp.Contrib if (c == '&') { entity.Append(c); -#if NET_4_0 - rawEntity.Append (c); -#endif state = 1; } else @@ -471,9 +351,6 @@ namespace RestSharp.Contrib state = 3; } entity.Append(c); -#if NET_4_0 - rawEntity.Append (c); -#endif } } else if (state == 2) @@ -488,20 +365,12 @@ namespace RestSharp.Contrib output.Append(key); state = 0; entity.Length = 0; -#if NET_4_0 - rawEntity.Length = 0; -#endif } } else if (state == 3) { if (c == ';') { -#if NET_4_0 - if (number == 0) - output.Append (rawEntity.ToString () + ";"); - else -#endif if (number > 65535) { output.Append("&#"); @@ -514,33 +383,21 @@ namespace RestSharp.Contrib } state = 0; entity.Length = 0; -#if NET_4_0 - rawEntity.Length = 0; -#endif have_trailing_digits = false; } else if (is_hex_value && Uri.IsHexDigit(c)) { number = number * 16 + Uri.FromHex(c); have_trailing_digits = true; -#if NET_4_0 - rawEntity.Append (c); -#endif } else if (Char.IsDigit(c)) { number = number * 10 + ((int)c - '0'); have_trailing_digits = true; -#if NET_4_0 - rawEntity.Append (c); -#endif } else if (number == 0 && (c == 'x' || c == 'X')) { is_hex_value = true; -#if NET_4_0 - rawEntity.Append (c); -#endif } else { @@ -568,11 +425,7 @@ namespace RestSharp.Contrib internal static bool NotEncoded(char c) { - return (c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_' -#if !NET_4_0 - || c == '\'' -#endif -); + return (c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_'); } internal static void UrlEncodeChar(char c, System.IO.Stream result, bool isUnicode) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index 34aa5e0ca..b4662d858 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -26,7 +26,7 @@ namespace Barotrauma public static void Convert() { - if (TextManager.Language != "English") + if (GameSettings.CurrentConfig.Language != TextManager.DefaultLanguage) { DebugConsole.ThrowError("Use the english localization when converting .csv to allow copying values"); return; @@ -123,8 +123,10 @@ namespace Barotrauma private static List ConvertInfoTextToXML(string[] csvContent, string language) { - List xmlContent = new List(); - xmlContent.Add(xmlHeader); + List xmlContent = new List + { + xmlHeader + }; string translatedName = GetTranslatedName(language); bool nowhitespace = TextManager.IsCJK(translatedName); @@ -151,6 +153,7 @@ namespace Barotrauma split[1] = split[2]; split[2] = string.Empty; } + split[1] = split[1].Replace(" & ", " & "); xmlContent.Add($"<{split[0]}>{split[1]}"); } else if (split[0].Contains(".") && !split[0].Any(char.IsUpper)) // An empty field @@ -220,15 +223,16 @@ namespace Barotrauma } //DebugConsole.NewMessage("Count: " + NPCPersonalityTrait.List.Count); - for (int i = 0; i < NPCPersonalityTrait.List.Count; i++) // Traits + var traits = NPCPersonalityTrait.GetAll(language.ToLanguageIdentifier()).ToArray(); + for (int i = 0; i < traits.Length; i++) // Traits { //string[] split = SplitCSV(csvContent[traitStart + i].Trim(separator)); string[] split = csvContent[traitStart + i].Split(separator); xmlContent.Add( $""); + $"{GetVariable("alloweddialogtags", string.Join(",", traits[i].AllowedDialogTags))}" + + $"{GetVariable("commonness", traits[i].Commonness.ToString(CultureInfo.InvariantCulture))}/>"); } xmlContent.Add(string.Empty); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 6327b9e26..2d4a1ffb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -53,7 +53,7 @@ namespace Barotrauma private static XElement ParseRecipe(ItemPrefab prefab) { - FabricationRecipe? recipe = prefab.FabricationRecipes.FirstOrDefault(); + FabricationRecipe? recipe = prefab.FabricationRecipes.Values.FirstOrDefault(); List ingredients = recipe?.RequiredItems.SelectMany(ri => ri.ItemPrefabs).Distinct().ToList() ?? new List(); Skill? skill = recipe?.RequiredSkills.FirstOrDefault(); @@ -61,7 +61,7 @@ namespace Barotrauma return new XElement("Recipe", new XAttribute("amount", recipe?.Amount ?? 0), new XAttribute("time", recipe?.RequiredTime ?? 0), - new XAttribute("skillname", skill?.Identifier ?? ""), + new XAttribute("skillname", skill?.Identifier.Value ?? ""), new XAttribute("skillamount", (int?) skill?.Level ?? 0), new XAttribute("ingredients", FormatArray(ingredients.Select(ip => ip.Name))), new XAttribute("values", FormatArray(ingredients.Select(ip => ip.DefaultPrice?.Price ?? 0))) @@ -80,15 +80,15 @@ namespace Barotrauma private static XElement ParseMedical(ItemPrefab prefab) { - XElement? itemMeleeWeapon = prefab.ConfigElement.GetChildElement(nameof(MeleeWeapon)); + ContentXElement? itemMeleeWeapon = prefab.ConfigElement.GetChildElement(nameof(MeleeWeapon)); // affliction, amount, duration - List> onSuccessAfflictions = new List>(); - List> onFailureAfflictions = new List>(); + List<(LocalizedString Name, float Amount, float Duration)> onSuccessAfflictions = new List<(LocalizedString Name, float Amount, float Duration)>(); + List<(LocalizedString Name, float Amount, float Duration)> onFailureAfflictions = new List<(LocalizedString Name, float Amount, float Duration)>(); int medicalRequiredSkill = 0; if (itemMeleeWeapon != null) { List statusEffects = new List(); - foreach (XElement subElement in itemMeleeWeapon.Elements()) + foreach (var subElement in itemMeleeWeapon.Elements()) { string name = subElement.Name.ToString(); if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) @@ -110,15 +110,15 @@ namespace Barotrauma foreach (StatusEffect statusEffect in successEffects) { float duration = statusEffect.Duration; - onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); - onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(ra => (GetAfflictionName(ra.AfflictionIdentifier), -ra.ReduceAmount, duration))); + onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => (affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } foreach (StatusEffect statusEffect in failureEffects) { float duration = statusEffect.Duration; - onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); - onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(ra => (GetAfflictionName(ra.AfflictionIdentifier), -ra.ReduceAmount, duration))); + onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => (affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } } @@ -141,15 +141,15 @@ namespace Barotrauma int skillRequirement = 0; // affliction, amount - List> damages = new List>(); + List<(LocalizedString Name, float Amount)> damages = new List<(LocalizedString Name, float Amount)>(); string[] validNames = { nameof(Projectile), nameof(MeleeWeapon), nameof(RepairTool), nameof(ItemComponent), nameof(RangedWeapon) }; - foreach (XElement icElement in prefab.ConfigElement.Elements()) + foreach (var icElement in prefab.ConfigElement.Elements()) { string icName = icElement.Name.ToString(); if (!validNames.Any(name => icName.Equals(name, StringComparison.OrdinalIgnoreCase))) { continue; } - foreach (XElement icChildElement in icElement.Elements()) + foreach (var icChildElement in icElement.Elements()) { string name = icChildElement.Name.ToString(); if (IsRequiredSkill(icChildElement, out Skill? skill) && skill != null) @@ -208,7 +208,7 @@ namespace Barotrauma continue; } - damages.Add(Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength)); + damages.Add((affliction.Prefab.Name, affliction.NonClampedStrength)); } } } @@ -224,9 +224,9 @@ namespace Barotrauma ); } - private static string GetAfflictionName(string identifier) + private static LocalizedString GetAfflictionName(Identifier identifier) { - return AfflictionPrefab.Prefabs.Find(prefab => prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))?.Name ?? CultureInfo.CurrentCulture.TextInfo.ToTitleCase(identifier.ToLower()); + return AfflictionPrefab.Prefabs.Find(prefab => prefab.Identifier == identifier)?.Name ?? CultureInfo.CurrentCulture.TextInfo.ToTitleCase(identifier.Value!.ToLower()); } private static string FormatFloat(float value) @@ -239,7 +239,7 @@ namespace Barotrauma return string.Join(separator, array); } - private static bool IsRequiredSkill(XElement element, out Skill? skill) + private static bool IsRequiredSkill(ContentXElement element, out Skill? skill) { string name = element.Name.ToString(); bool isSkill = name.Equals("RequiredSkill", StringComparison.OrdinalIgnoreCase) || @@ -247,7 +247,7 @@ namespace Barotrauma if (isSkill) { - string identifier = element.GetAttributeString(nameof(Skill.Identifier).ToLowerInvariant(), string.Empty); + Identifier identifier = element.GetAttributeIdentifier(nameof(Skill.Identifier), Identifier.Empty); float level = element.GetAttributeFloat(nameof(Skill.Level).ToLowerInvariant(), 0f); skill = new Skill(identifier, level); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index ed5a63cac..7990dee16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -283,7 +283,7 @@ namespace Barotrauma indexBuffer?.Dispose(); indexBuffer = new IndexBuffer(gfxDevice, IndexElementSize.SixteenBits, requiredIndexCount * 2, BufferUsage.WriteOnly); ushort[] indices = new ushort[requiredIndexCount * 2]; - for (int i=0;i LimitString((LocalizedString)str, font, maxWidth); + public static string LimitString(string str, ScalableFont font, int maxWidth) { if (maxWidth <= 0 || string.IsNullOrWhiteSpace(str)) return ""; @@ -434,6 +443,11 @@ namespace Barotrauma return Color.Lerp(gradient[(int)scaledT], gradient[(int)Math.Min(scaledT + 1, gradient.Length - 1)], (scaledT - (int)scaledT)); } + public static LocalizedString WrapText(LocalizedString text, float lineLength, GUIFont font, float textScale = 1.0f) + { + return new WrappedLString(text, lineLength, font, textScale); + } + public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f) => font.WrapText(text, lineLength / textScale); @@ -464,5 +478,15 @@ namespace Barotrauma if (b.Build < a.Build) { return false; } return false; } + + public static void OpenFileWithShell(string filename) + { + ProcessStartInfo startInfo = new ProcessStartInfo() + { + FileName = filename, + UseShellExecute = true + }; + Process.Start(startInfo); + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 9948738a1..31debdb9c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,24 +6,36 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + DEBUG;TRACE;CLIENT;LINUX;USE_STEAM x64 ..\bin\$(Configuration)Linux\ + net6.0 + 8 TRACE;DEBUG;CLIENT;LINUX;X64;USE_STEAM x64 ..\bin\$(Configuration)Linux\ + net6.0 + 8 @@ -95,7 +107,7 @@ Icon.bmp - + @@ -140,7 +152,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 4d84c5305..6f1f04661 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,13 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico - 0.9.703.0 Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 @@ -62,6 +62,12 @@ + + SharedSource\Prefabs\PrefabSelector.cs + + + SharedSource\Prefabs\PrefabCollectionSubset.cs + diff --git a/Barotrauma/BarotraumaClient/Properties/launchSettings.json b/Barotrauma/BarotraumaClient/Properties/launchSettings.json index cc9cae3a3..45cb9cb6b 100644 --- a/Barotrauma/BarotraumaClient/Properties/launchSettings.json +++ b/Barotrauma/BarotraumaClient/Properties/launchSettings.json @@ -3,6 +3,14 @@ "WindowsClient": { "commandName": "Project", "nativeDebugging": false + }, + "MacClient": { + "commandName": "Project", + "nativeDebugging": false + }, + "LinuxClient": { + "commandName": "Project", + "nativeDebugging": false } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index afa31a7c3..ca8ab114b 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,13 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable app.manifest + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 @@ -65,6 +66,12 @@ + + SharedSource\Steam\AuthTicket.cs + + + SharedSource\Utils\Result.cs + @@ -120,6 +127,9 @@ + + SharedSource\Utils\Result + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c60226b92..996817458 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,24 +6,36 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + DEBUG;TRACE;SERVER;LINUX;USE_STEAM x64 ..\bin\$(Configuration)Linux\ + net6.0 + 8 TRACE;DEBUG;SERVER;LINUX;X64;USE_STEAM x64 ..\bin\$(Configuration)Linux\ + net6.0 + 8 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 42d0facc0..296dc0707 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,13 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico - 0.9.0.0 Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 35047f575..4a9e5f918 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -7,8 +7,6 @@ namespace Barotrauma { public static Character Controlled = null; - partial void InitProjSpecific(XElement mainElement) { } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, stun, attackResult.Afflictions); @@ -20,7 +18,7 @@ namespace Barotrauma { if (causeOfDeath == CauseOfDeathType.Affliction) { - GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); + GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name.Value + ")", ServerLog.MessageType.Attack); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 91e38cf04..c3d14550f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -8,9 +8,9 @@ namespace Barotrauma { partial class CharacterInfo { - private readonly Dictionary prevSentSkill = new Dictionary(); + private readonly Dictionary prevSentSkill = new Dictionary(); - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel) + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (Character == null || Character.Removed) { return; } if (!prevSentSkill.ContainsKey(skillIdentifier)) @@ -45,16 +45,18 @@ namespace Barotrauma msg.Write(ID); msg.Write(Name); msg.Write(OriginalName); - msg.Write((byte)Gender); - msg.Write((byte)Race); - msg.Write((byte)HeadSpriteId); - msg.Write((byte)HairIndex); - msg.Write((byte)BeardIndex); - msg.Write((byte)MoustacheIndex); - msg.Write((byte)FaceAttachmentIndex); - msg.WriteColorR8G8B8(SkinColor); - msg.WriteColorR8G8B8(HairColor); - msg.WriteColorR8G8B8(FacialHairColor); + msg.Write((byte)Head.Preset.TagSet.Count); + foreach (Identifier tag in Head.Preset.TagSet) + { + msg.Write(tag); + } + msg.Write((byte)Head.HairIndex); + msg.Write((byte)Head.BeardIndex); + msg.Write((byte)Head.MoustacheIndex); + msg.Write((byte)Head.FaceAttachmentIndex); + msg.WriteColorR8G8B8(Head.SkinColor); + msg.WriteColorR8G8B8(Head.HairColor); + msg.WriteColorR8G8B8(Head.FacialHairColor); msg.Write(ragdollFileName); if (Job != null) @@ -73,20 +75,9 @@ namespace Barotrauma msg.Write(""); msg.Write((byte)0); } - // TODO: animations - msg.Write((byte)SavedStatValues.SelectMany(s => s.Value).Count()); - foreach (var savedStatValuePair in SavedStatValues) - { - foreach (var savedStatValue in savedStatValuePair.Value) - { - msg.Write((byte)savedStatValuePair.Key); - msg.Write(savedStatValue.StatIdentifier); - msg.Write(savedStatValue.StatValue); - msg.Write(savedStatValue.RemoveOnDeath); - } - } + msg.Write((ushort)ExperiencePoints); - msg.Write((ushort)AdditionalTalentPoints); + msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 7eb20a6f5..a7c694e5d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -283,12 +283,12 @@ namespace Barotrauma // get the full list of talents from the player, only give the ones // that are not already given (or otherwise not viable) ushort talentCount = msg.ReadUInt16(); - List talentSelection = new List(); + List talentSelection = new List(); for (int i = 0; i < talentCount; i++) { UInt32 talentIdentifier = msg.ReadUInt32(); - var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UIntIdentifier == talentIdentifier); - if (prefab == null) { continue; } + var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UintIdentifier == talentIdentifier); + if (prefab == null) { continue; } if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) { @@ -381,28 +381,27 @@ namespace Barotrauma if (type == 1) { var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); - bool validOrder = currentOrderInfo.HasValue; + bool validOrder = currentOrderInfo != null; msg.Write(validOrder); if (!validOrder) { break; } - var orderPrefab = currentOrderInfo.Value.Order.Prefab; - int orderIndex = Order.PrefabList.IndexOf(orderPrefab); - msg.WriteRangedInteger(orderIndex, 0, Order.PrefabList.Count); + var orderPrefab = currentOrderInfo.Prefab; + msg.Write(orderPrefab.UintIdentifier); if (!orderPrefab.HasOptions) { break; } - int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Value.OrderOption); + int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option); if (optionIndex == -1) { - DebugConsole.AddWarning($"Error while writing order data. Order option \"{(currentOrderInfo.Value.OrderOption ?? null)}\" not found in the order prefab \"{orderPrefab.Name}\"."); + DebugConsole.AddWarning($"Error while writing order data. Order option \"{currentOrderInfo.Option}\" not found in the order prefab \"{orderPrefab.Name}\"."); } msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length); } else if (type == 2) { var objective = controller.ObjectiveManager.CurrentObjective; - bool validObjective = !string.IsNullOrEmpty(objective?.Identifier); + bool validObjective = objective != null && objective.Identifier != Identifier.Empty; msg.Write(validObjective); if (!validObjective) { break; } msg.Write(objective.Identifier); - msg.Write(objective.Option ?? ""); + msg.Write(objective.Option); UInt16 targetEntityId = 0; if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) { @@ -435,7 +434,7 @@ namespace Barotrauma foreach (var unlockedTalent in characterTalents) { msg.Write(unlockedTalent.AddedThisRound); - msg.Write(unlockedTalent.Prefab.UIntIdentifier); + msg.Write(unlockedTalent.Prefab.UintIdentifier); } break; case NetEntityEvent.Type.UpdateMoney: @@ -623,9 +622,9 @@ namespace Barotrauma public void WriteSpawnData(IWriteMessage msg, UInt16 entityId, bool restrictMessageSize) { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } - int msgLength = msg.LengthBytes; + int initialMsgLength = msg.LengthBytes; msg.Write(Info == null); msg.Write(entityId); @@ -671,43 +670,60 @@ namespace Barotrauma msg.Write((byte)TeamID); msg.Write(this is AICharacter); msg.Write(info.SpeciesName); + int msgLengthBeforeInfo = msg.LengthBytes; info.ServerWrite(msg); + int infoLength = msg.LengthBytes - msgLengthBeforeInfo; msg.Write((byte)CampaignInteractionType); - + int msgLengthBeforeOrders = msg.LengthBytes; // Current orders - msg.Write((byte)info.CurrentOrders.Count(o => o.Order != null)); + msg.Write((byte)info.CurrentOrders.Count(o => o != null)); foreach (var orderInfo in info.CurrentOrders) { - if (orderInfo.Order == null) { continue; } - msg.Write((byte)Order.PrefabList.IndexOf(orderInfo.Order.Prefab)); - msg.Write(orderInfo.Order.TargetEntity == null ? (UInt16)0 : orderInfo.Order.TargetEntity.ID); - var hasOrderGiver = orderInfo.Order.OrderGiver != null; + if (orderInfo == null) { continue; } + msg.Write(orderInfo.Prefab.UintIdentifier); + msg.Write(orderInfo.TargetEntity == null ? (UInt16)0 : orderInfo.TargetEntity.ID); + var hasOrderGiver = orderInfo.OrderGiver != null; msg.Write(hasOrderGiver); - if (hasOrderGiver) { msg.Write(orderInfo.Order.OrderGiver.ID); } - msg.Write((byte)(string.IsNullOrWhiteSpace(orderInfo.OrderOption) ? 0 : Array.IndexOf(orderInfo.Order.Prefab.Options, orderInfo.OrderOption))); + if (hasOrderGiver) { msg.Write(orderInfo.OrderGiver.ID); } + msg.Write((byte)(orderInfo.Option == Identifier.Empty ? 0 : orderInfo.Prefab.Options.IndexOf(orderInfo.Option))); msg.Write((byte)orderInfo.ManualPriority); - var hasTargetPosition = orderInfo.Order.TargetPosition != null; + var hasTargetPosition = orderInfo.TargetPosition != null; msg.Write(hasTargetPosition); if (hasTargetPosition) { - msg.Write(orderInfo.Order.TargetPosition.Position.X); - msg.Write(orderInfo.Order.TargetPosition.Position.Y); - msg.Write(orderInfo.Order.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.Order.TargetPosition.Hull.ID); + msg.Write(orderInfo.TargetPosition.Position.X); + msg.Write(orderInfo.TargetPosition.Position.Y); + msg.Write(orderInfo.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.TargetPosition.Hull.ID); } } + int ordersLength = msg.LengthBytes - msgLengthBeforeOrders; + + if (msg.LengthBytes - initialMsgLength >= 255 && restrictMessageSize) + { + string errorMsg = $"Error when writing character spawn data: data exceeded 255 bytes (info: {infoLength}, orders: {ordersLength}, total: {msg.LengthBytes - initialMsgLength})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchData", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } TryWriteStatus(msg); void TryWriteStatus(IWriteMessage msg) { + int msgLengthBeforeStatus = msg.LengthBytes - initialMsgLength; + var tempBuffer = new ReadWriteMessage(); WriteStatus(tempBuffer); - if (msg.LengthBytes + tempBuffer.LengthBytes >= 255 && restrictMessageSize) + if (msgLengthBeforeStatus + tempBuffer.LengthBytes >= 255 && restrictMessageSize) { msg.Write(false); - DebugConsole.ThrowError($"Error when writing character spawn data: status data caused the length of the message to exceed 255 bytes ({msg.LengthBytes} + {tempBuffer.LengthBytes})"); + if (msgLengthBeforeStatus < 255) + { + string errorMsg = $"Error when writing character spawn data: status data caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchDataForStatus", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } } else { @@ -716,7 +732,7 @@ namespace Barotrauma } } - DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - msgLength)); + DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - initialMsgLength)); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 1ff909090..c3ad6d244 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -98,7 +98,7 @@ namespace Barotrauma { ColoredText msg = queuedMessages.Dequeue(); Messages.Add(msg); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(msg); if (unsavedMessages.Count >= messagesPerFile) @@ -281,7 +281,7 @@ namespace Barotrauma { var msg = queuedMessages.Dequeue(); Messages.Add(msg); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(msg); if (unsavedMessages.Count >= messagesPerFile) @@ -392,7 +392,7 @@ namespace Barotrauma if (float.TryParse(args[0], out seconds)) { seconds = Math.Max(0, seconds); - GameMain.Server.SendConsoleMessage("Set kill disconnected timer to " + ToolBox.SecondsToReadableTime(seconds), client); + GameMain.Server.SendConsoleMessage("Set kill disconnected timer to " + ToolBox.SecondsToReadableTime(seconds).Value, client); NewMessage(client.Name + " set kill disconnected timer to " + ToolBox.SecondsToReadableTime(seconds), Color.White); } else @@ -955,7 +955,7 @@ namespace Barotrauma return; } client.Muted = true; - GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer"), client, ChatMessageType.MessageBox); + GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer").Value, client, ChatMessageType.MessageBox); }, () => { @@ -975,7 +975,7 @@ namespace Barotrauma return; } client.Muted = false; - GameMain.Server.SendDirectChatMessage(TextManager.Get("UnmutedByServer"), client, ChatMessageType.MessageBox); + GameMain.Server.SendDirectChatMessage(TextManager.Get("UnmutedByServer").Value, client, ChatMessageType.MessageBox); }, () => { @@ -1102,7 +1102,7 @@ namespace Barotrauma TraitorManager traitorManager = GameMain.Server.TraitorManager; if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) { - GameMain.Server.SendTraitorMessage(client, "There are no traitors at the moment.", "", TraitorMessageType.Console); + GameMain.Server.SendTraitorMessage(client, "There are no traitors at the moment.", Identifier.Empty, TraitorMessageType.Console); return; } foreach (Traitor t in traitorManager.Traitors) @@ -1116,11 +1116,11 @@ namespace Barotrauma $"[traitorgoals]={traitorGoals.Substring(traitorGoalsStart)}", $"[traitorname]={t.Character.Name}", "Traitor [traitorname]'s current goals are:\n[traitorgoals]" - }.Where(s => !string.IsNullOrEmpty(s))), t.Mission?.Identifier, TraitorMessageType.Console); + }.Where(s => !string.IsNullOrEmpty(s))), t.Mission.Identifier, TraitorMessageType.Console); } else { - GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", "", t.Character.Name), "", TraitorMessageType.Console); + GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", "", t.Character.Name), Identifier.Empty, TraitorMessageType.Console); } } //GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console); @@ -1296,7 +1296,7 @@ namespace Barotrauma { return new string[][] { - GameModePreset.List.Select(gm => gm.Name).ToArray() + GameModePreset.List.Select(gm => gm.Name.Value).ToArray() }; })); @@ -1668,7 +1668,7 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + a.Identifier == args[0]); if (afflictionPrefab == null) { GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client, Color.Red); @@ -1756,7 +1756,7 @@ namespace Barotrauma if (targetCharacter == null) { return; } TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => - c.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + c.Identifier == args[0] || c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { @@ -1789,7 +1789,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage($"Failed to find the job \"{args[0]}\".", client, Color.Red); return; } - if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) + if (!TalentTree.JobTalentTrees.TryGet(job.Identifier, out TalentTree talentTree)) { GameMain.Server.SendConsoleMessage($"No talents configured for the job \"{args[0]}\".", client, Color.Red); return; @@ -2010,7 +2010,7 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage("Assigned the rank \"" + preset.Name + "\" to " + client.Name + ".", senderClient); + GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient); NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); } ); @@ -2158,7 +2158,7 @@ namespace Barotrauma foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { if (permission == ClientPermissions.None || !client.HasPermission(permission)) { continue; } - GameMain.Server.SendConsoleMessage(" - " + TextManager.Get("ClientPermission." + permission), senderClient); + GameMain.Server.SendConsoleMessage($" - {TextManager.Get("ClientPermission." + permission)}", senderClient); } if (client.HasPermission(ClientPermissions.ConsoleCommands)) { @@ -2257,7 +2257,7 @@ namespace Barotrauma var tagList = MapEntityPrefab.List.SelectMany(p => p.Tags.Select(t => t)).Distinct(); foreach (var tag in tagList) { - NewMessage(tag, Color.Yellow); + NewMessage(tag.Value, Color.Yellow); } })); @@ -2298,7 +2298,7 @@ namespace Barotrauma return; } - string skillIdentifier = args[0]; + Identifier skillIdentifier = args[0].ToIdentifier(); string levelString = args[1]; Character character = args.Length >= 3 ? FindMatchingCharacter(args.Skip(2).ToArray(), false) : senderClient.Character; @@ -2313,7 +2313,7 @@ namespace Barotrauma if (float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out float level) || isMax) { if (isMax) { level = 100; } - if (skillIdentifier.Equals("all", StringComparison.OrdinalIgnoreCase)) + if (skillIdentifier == "all") { foreach (Skill skill in character.Info.Job.Skills) { @@ -2397,7 +2397,7 @@ namespace Barotrauma { GameMain.Server.CreateEntityEvent(c, new object[] { NetEntityEvent.Type.Status }); }*/ - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { GameMain.Server.CreateEntityEvent(hull); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 7569c73c0..718c588b9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -80,7 +80,7 @@ namespace Barotrauma partial void ShowDialog(Character speaker, Character targetCharacter) { targetClients.Clear(); - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { IEnumerable entities = ParentEvent.GetTargets(TargetTag); foreach (Entity e in entities) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs index 99e79fc47..1ab83b7ab 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -15,7 +15,7 @@ namespace Barotrauma msg.Write((ushort)spawnedItems.Count); foreach (Item item in spawnedItems) { - item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } msg.Write((byte)characters.Count); @@ -27,7 +27,7 @@ namespace Barotrauma msg.Write((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index 10f5ce5ad..6b54790d7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -13,7 +13,8 @@ namespace Barotrauma item.WriteSpawnData(msg, item.ID, parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, - parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0); + parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0, + inventorySlotIndices.ContainsKey(item) ? inventorySlotIndices[item] : -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 5ac067bf3..5446c04e1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -11,11 +11,11 @@ namespace Barotrauma private bool initialized = false; - public override string Description + public override LocalizedString Description { get { - if (descriptions == null) return ""; + if (descriptions == null) { return ""; } //non-team-specific description return descriptions[0]; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs index 060369f9a..dea3acc83 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs @@ -24,7 +24,7 @@ namespace Barotrauma msg.Write((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index b3dd48bff..ada2e763a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -16,11 +16,11 @@ namespace Barotrauma foreach (var kvp in spawnedResources) { msg.Write((byte)kvp.Value.Count); - var rotation = resourceClusters[kvp.Key].rotation; + var rotation = resourceClusters[kvp.Key].Rotation; msg.Write(rotation); foreach (var r in kvp.Value) { - r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0); + r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index fc1b5041c..064ccd3e3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -7,13 +7,13 @@ namespace Barotrauma partial void ShowMessageProjSpecific(int missionState) { int messageIndex = missionState - 1; - if (messageIndex >= Headers.Count && messageIndex >= Messages.Count) { return; } + if (messageIndex >= Headers.Length && messageIndex >= Messages.Length) { return; } if (messageIndex < 0) { return; } - string header = messageIndex < Headers.Count ? Headers[messageIndex] : ""; - string message = messageIndex < Messages.Count ? Messages[messageIndex] : ""; + LocalizedString header = messageIndex < Headers.Length ? Headers[messageIndex] : ""; + LocalizedString message = messageIndex < Messages.Length ? Messages[messageIndex] : ""; - GameServer.Log(TextManager.Get("MissionInfo") + ": " + header + " - " + message, ServerLog.MessageType.ServerMessage); + GameServer.Log($"{TextManager.Get("MissionInfo")}: {header} - {message}", ServerLog.MessageType.ServerMessage); } public virtual void ServerWriteInitial(IWriteMessage msg, Client c) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs index 7d5c88bb1..f8e974834 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -15,7 +15,7 @@ namespace Barotrauma msg.Write((ushort)items.Count); foreach (Item item in items) { - item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs index 4eb529c2e..c04b48601 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs @@ -24,7 +24,7 @@ namespace Barotrauma msg.Write((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 292d53ce0..cdc8e3622 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -10,6 +10,7 @@ namespace Barotrauma private UInt16 originalInventoryID; private byte originalItemContainerIndex; + private int originalSlotIndex; private readonly List> executedEffectIndices = new List>(); @@ -24,7 +25,7 @@ namespace Barotrauma } else { - item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex); + item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex, originalSlotIndex); } msg.Write((byte)executedEffectIndices.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs index dc5dbff31..010ed0224 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs @@ -13,7 +13,8 @@ namespace Barotrauma item.WriteSpawnData(msg, item.ID, parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, - parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0); + parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0, + inventorySlotIndices.ContainsKey(item) ? inventorySlotIndices[item] : -1); } ServerWriteScanTargetStatus(msg); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 048ef43a9..236257000 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -20,7 +21,6 @@ namespace Barotrauma public static bool IsSingleplayer => NetworkMember == null; public static bool IsMultiplayer => NetworkMember != null; - private static World world; public static World World { @@ -31,7 +31,6 @@ namespace Barotrauma } set { world = value; } } - public static GameSettings Config; public static GameServer Server; public static NetworkMember NetworkMember @@ -58,28 +57,14 @@ namespace Barotrauma //TODO: maybe clean up instead of having these constants public static readonly Screen SubEditorScreen = UnimplementedScreen.Instance; - public static DecalManager DecalManager; - public static bool ShouldRun = true; private static Stopwatch stopwatch; - private static Queue prevUpdateRates = new Queue(); + private static readonly Queue prevUpdateRates = new Queue(); private static int updateCount = 0; - - private static ContentPackage vanillaContent; - public static ContentPackage VanillaContent - { - get - { - if (vanillaContent == null) - { - // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.CorePackages.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); - } - return vanillaContent; - } - } + + public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage; public readonly string[] CommandLineArgs; @@ -96,13 +81,14 @@ namespace Barotrauma FarseerPhysics.Settings.PositionIterations = 1; Console.WriteLine("Loading game settings"); - Config = new GameSettings(); + GameSettings.Init(); Console.WriteLine("Loading MD5 hash cache"); - Md5Hash.LoadCache(); + Md5Hash.Cache.Load(); Console.WriteLine("Initializing SteamManager"); SteamManager.Initialize(); + //TODO: figure out how consent is supposed to work for servers //Console.WriteLine("Initializing GameAnalytics"); //GameAnalyticsManager.InitIfConsented(); @@ -115,36 +101,11 @@ namespace Barotrauma public void Init() { - NPCSet.LoadSets(); - FactionPrefab.LoadFactions(); - CharacterPrefab.LoadAll(); - MissionPrefab.Init(); - TraitorMissionPrefab.Init(); - MapEntityPrefab.Init(); - MapGenerationParams.Init(); - LevelGenerationParams.LoadPresets(); - CaveGenerationParams.LoadPresets(); - OutpostGenerationParams.LoadPresets(); - EventSet.LoadPrefabs(); - Order.Init(); - EventManagerSettings.Init(); - ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); - AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); - SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); - StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); - UpgradePrefab.LoadAll(GetFilesOfType(ContentType.UpgradeModules)); - JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); - CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); - NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); - ItemAssemblyPrefab.LoadAll(); - LevelObjectPrefab.LoadAll(); - BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); - TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); - TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); + CoreEntityPrefab.InitCorePrefabs(); GameModePreset.Init(); - DecalManager = new DecalManager(); - LocationType.Init(); + + ContentPackageManager.Init().Consume(); SubmarineInfo.RefreshSavedSubs(); @@ -180,23 +141,6 @@ namespace Barotrauma }*/ } - /// - /// Returns the file paths of all files of the given type in the content packages. - /// - /// - /// If true, also returns files in content packages that are installed but not currently selected. - public IEnumerable GetFilesOfType(ContentType type, bool searchAllContentPackages = false) - { - if (searchAllContentPackages) - { - return ContentPackage.GetFilesOfType(ContentPackage.AllPackages, type); - } - else - { - return ContentPackage.GetFilesOfType(Config.AllEnabledPackages, type); - } - } - public bool TryStartChildServerRelay() { for (int i = 0; i < CommandLineArgs.Length; i++) @@ -383,6 +327,9 @@ namespace Barotrauma //otherwise it snowballs and becomes unplayable Timing.Accumulator = Timing.Step; } + + CrossThread.ProcessTasks(); + prevTicks = currTicks; while (Timing.Accumulator >= Timing.Step) { @@ -455,7 +402,8 @@ namespace Barotrauma SaveUtil.CleanUnnecessarySaveFiles(); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } MainThread = null; diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 1fe87b072..b888b2139 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -63,12 +63,12 @@ namespace Barotrauma if (!item.Removed && canAddToRemoveQueue && Entity.FindEntityByID(item.ID) is Item entity) { item.Removed = true; - Entity.Spawner.AddToRemoveQueue(entity); + Entity.Spawner.AddItemToRemoveQueue(entity); } SoldItems.Add(item); Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; - GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); } OnSoldItemsChanged?.Invoke(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 3dcc595ba..541435d72 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -46,14 +46,14 @@ namespace Barotrauma public void ServerWriteActiveOrders(IWriteMessage msg) { - ushort count = (ushort)ActiveOrders.Count(o => o.First != null && !o.Second.HasValue); + ushort count = (ushort)ActiveOrders.Count(o => o.Order != null && !o.FadeOutTime.HasValue); msg.Write(count); if (count > 0) { foreach (var activeOrder in ActiveOrders) { - if (!(activeOrder?.First is Order order) || activeOrder.Second.HasValue) { continue; } - OrderChatMessage.WriteOrder(msg, order, targetCharacter: null, order.TargetSpatialEntity, orderOption: null, orderPriority: 0, order.WallSectionIndex, isNewOrder: true); + if (!(activeOrder?.Order is Order order) || activeOrder.FadeOutTime.HasValue) { continue; } + OrderChatMessage.WriteOrder(msg, order, null, isNewOrder: true); bool hasOrderGiver = order.OrderGiver != null; msg.Write(hasOrderGiver); if (hasOrderGiver) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 9c3d14d77..a2bfc7450 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -14,8 +14,8 @@ namespace Barotrauma { foreach (Mission mission in Missions) { - GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, ServerLog.MessageType.ServerMessage); - GameServer.Log(mission.Description, ServerLog.MessageType.ServerMessage); + GameServer.Log($"{TextManager.Get("Mission")}: {mission.Name}", ServerLog.MessageType.ServerMessage); + GameServer.Log(mission.Description.Value, ServerLog.MessageType.ServerMessage); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 8013b965b..f8c39e187 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -94,7 +94,7 @@ namespace Barotrauma { throw new System.InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); } - character.SpawnInventoryItems(inventory, itemData); + character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); } public void ApplyHealthData(Character character) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs index 51ad5e730..56c27906a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs @@ -6,8 +6,8 @@ { foreach (Mission mission in missions) { - Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); - Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log($"{TextManager.Get("Mission")}: {mission.Name}", Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log(mission.Description.Value, Networking.ServerLog.MessageType.ServerMessage); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index fc2eaf90e..f736290f3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -607,7 +607,7 @@ namespace Barotrauma foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) { msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? string.Empty); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); } var characterData = GetClientCharacterData(c); @@ -681,10 +681,10 @@ namespace Barotrauma List purchasedUpgrades = new List(); for (int i = 0; i < purchasedUpgradeCount; i++) { - string upgradeIdentifier = msg.ReadString(); + Identifier upgradeIdentifier = msg.ReadIdentifier(); UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); - string categoryIdentifier = msg.ReadString(); + Identifier categoryIdentifier = msg.ReadIdentifier(); UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); int upgradeLevel = msg.ReadByte(); @@ -698,8 +698,8 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - string itemToInstallIdentifier = msg.ReadString(); - ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + Identifier itemToInstallIdentifier = msg.ReadIdentifier(); + ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs index 460c5ec46..6548e6046 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs @@ -9,11 +9,11 @@ namespace Barotrauma.Items.Components msg.Write(tainted); if (tainted) { - msg.Write(selectedTaintedEffect?.UIntIdentifier ?? 0); + msg.Write(selectedTaintedEffect?.UintIdentifier ?? 0); } else { - msg.Write(selectedEffect?.UIntIdentifier ?? 0); + msg.Write(selectedEffect?.UintIdentifier ?? 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs index b4490c121..cac426158 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -9,9 +9,9 @@ namespace Barotrauma.Items.Components private const int serverHealthUpdateDelay = 10; private int serverHealthUpdateTimer; - partial void LoadVines(XElement element) + partial void LoadVines(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs index 7235d11c7..25d01e567 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class ItemComponent : ISerializableEntity { - private bool LoadElemProjSpecific(XElement subElement) + private bool LoadElemProjSpecific(ContentXElement subElement) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index be9d09f71..912920409 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -11,28 +11,28 @@ namespace Barotrauma.Items.Components private string lastSentText; private float sendStateTimer; - [Serialize("", true, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)] + [Serialize("", IsPropertySaveable.Yes, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)] public string Text { get; set; } - [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label.", alwaysUseInstanceValues: true)] + [Editable, Serialize("0,0,0,255", IsPropertySaveable.Yes, description: "The color of the text displayed on the label.", alwaysUseInstanceValues: true)] public Color TextColor { get; set; } - [Editable, Serialize(1.0f, true, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] + [Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] public float TextScale { get; set; } - [Serialize("0,0,0,0", true, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] public Vector4 Padding { get; @@ -44,7 +44,7 @@ namespace Barotrauma.Items.Components //do nothing } - public ItemLabel(Item item, XElement element) + public ItemLabel(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index 335137727..baddfb0d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -11,23 +11,23 @@ namespace Barotrauma.Items.Components { public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { - int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1); + uint recipeHash = msg.ReadUInt32(); item.CreateServerEvent(this); if (!item.CanClientAccess(c)) return; - if (itemIndex == -1) + if (recipeHash == 0) { CancelFabricating(c.Character); } else { //if already fabricating the selected item, return - if (fabricatedItem != null && fabricationRecipes.IndexOf(fabricatedItem) == itemIndex) return; - if (itemIndex < 0 || itemIndex >= fabricationRecipes.Count) return; + if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; } + if (recipeHash == 0) { return; } - StartFabricating(fabricationRecipes[itemIndex], c.Character); + StartFabricating(fabricationRecipes[recipeHash], c.Character); } } @@ -48,9 +48,9 @@ namespace Barotrauma.Items.Components FabricatorState stateAtEvent = (FabricatorState)extraData[3]; msg.Write((byte)stateAtEvent); msg.Write(timeUntilReady); - int itemIndex = fabricatedItem == null ? -1 : fabricationRecipes.IndexOf(fabricatedItem); - msg.WriteRangedInteger(itemIndex, -1, fabricationRecipes.Count - 1); - UInt16 userID = fabricatedItem == null || user == null ? (UInt16)0 : user.ID; + uint recipeHash = fabricatedItem?.RecipeHash ?? 0; + msg.Write(recipeHash); + UInt16 userID = fabricatedItem is null || user is null ? (UInt16)0 : user.ID; msg.Write(userID); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 5f9b0385b..673f53972 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -4,7 +4,10 @@ namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { - void InitProjSpecific() + private Character prevLoggedFixer; + private FixActions prevLoggedFixAction; + + partial void InitProjSpecific(ContentXElement _) { //let the clients know the initial deterioration delay item.CreateServerEvent(this); @@ -19,7 +22,7 @@ namespace Barotrauma.Items.Components { if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log($"Non traitor \"{c.Character.Name}\" attempted to sabotage item."); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 338c9ce7f..ccf34fa50 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -33,7 +33,7 @@ namespace Barotrauma { accessible = false; } - else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead) + else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead && !characterInventory.AccessibleByOwner) { accessible = false; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 918bfde23..e229c8ed9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -2,6 +2,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -14,7 +15,7 @@ namespace Barotrauma public override Sprite Sprite { - get { return prefab?.sprite; } + get { return base.Prefab?.Sprite; } } partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) @@ -232,14 +233,14 @@ namespace Barotrauma } } - public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex) + public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex, int originalSlotIndex) { if (GameMain.Server == null) { return; } msg.Write(Prefab.OriginalName); msg.Write(Prefab.Identifier); - msg.Write(Description != prefab.Description); - if (Description != prefab.Description) + msg.Write(Description != base.Prefab.Description); + if (Description != base.Prefab.Description) { msg.Write(Description); } @@ -259,9 +260,7 @@ namespace Barotrauma { msg.Write(originalInventoryID); msg.Write(originalItemContainerIndex); - - int slotIndex = ParentInventory.FindIndex(this); - msg.Write(slotIndex < 0 ? (byte)255 : (byte)slotIndex); + msg.Write(originalSlotIndex < 0 ? (byte)255 : (byte)originalSlotIndex); } msg.Write(body == null ? (byte)0 : (byte)body.BodyType); @@ -285,13 +284,13 @@ namespace Barotrauma } msg.Write(teamID); - bool tagsChanged = tags.Count != prefab.Tags.Count || !tags.All(t => prefab.Tags.Contains(t)); + bool tagsChanged = tags.Count != base.Prefab.Tags.Count || !tags.All(t => base.Prefab.Tags.Contains(t)); msg.Write(tagsChanged); if (tagsChanged) { - string[] splitTags = Tags.Split(','); - msg.Write(string.Join(',', splitTags.Where(t => !prefab.Tags.Contains(t)))); - msg.Write(string.Join(',', prefab.Tags.Where(t => !splitTags.Contains(t)))); + IEnumerable splitTags = Tags.Split(',').ToIdentifiers(); + msg.Write(string.Join(',', splitTags.Where(t => !base.Prefab.Tags.Contains(t)))); + msg.Write(string.Join(',', base.Prefab.Tags.Where(t => !splitTags.Contains(t)))); } var nameTag = GetComponent(); msg.Write(nameTag != null); @@ -386,7 +385,7 @@ namespace Barotrauma if (!ItemList.Contains(this)) { string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); + DebugConsole.AddWarning(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 6c7bf6616..65beaf5ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -7,9 +7,11 @@ namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { - partial void LoadPrefab(XElement element) + private float damageUpdateTimer; + + partial void LoadPrefab(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -29,7 +31,24 @@ namespace Barotrauma.MapCreatures.Behavior } } - + + partial void UpdateDamage(float deltaTime) + { + if (damageUpdateTimer <= 0) + { + foreach (BallastFloraBranch branch in Branches) + { + if (Math.Abs(branch.AccumulatedDamage) > 1.0f) + { + SendNetworkMessage(this, NetworkHeader.BranchDamage, branch); + branch.AccumulatedDamage = 0f; + } + } + damageUpdateTimer = 1f; + } + damageUpdateTimer -= deltaTime; + } + public void ServerWriteSpawn(IWriteMessage msg) { msg.Write(Prefab.Identifier); @@ -49,12 +68,12 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write((ushort)branch.MaxHealth); msg.Write((int)(x / VineTile.Size)); msg.Write((int)(y / VineTile.Size)); + msg.Write(branch.ParentBranch == null ? -1 : Branches.IndexOf(branch.ParentBranch)); } - public void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch, float damage) + public void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch) { msg.Write((int)branch.ID); - msg.Write(damage); msg.Write(branch.Health); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 827171ade..861494dc6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -82,12 +82,13 @@ namespace Barotrauma behavior.ServerWriteSpawn(message); break; case BallastFloraBehavior.NetworkHeader.Kill: + case BallastFloraBehavior.NetworkHeader.Remove: break; case BallastFloraBehavior.NetworkHeader.BranchCreate when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is int parentId: behavior.ServerWriteBranchGrowth(message, branch, parentId); break; - case BallastFloraBehavior.NetworkHeader.BranchDamage when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is float damage: - behavior.ServerWriteBranchDamage(message, branch, damage); + case BallastFloraBehavior.NetworkHeader.BranchDamage when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch: + behavior.ServerWriteBranchDamage(message, branch); break; case BallastFloraBehavior.NetworkHeader.BranchRemove when extraData.Length >= 3 && extraData[2] is BallastFloraBranch branch: behavior.ServerWriteBranchRemove(message, branch); @@ -147,7 +148,7 @@ namespace Barotrauma message.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); foreach (Decal decal in decals) { - message.Write(decal.Prefab.UIntIdentifier); + message.Write(decal.Prefab.UintIdentifier); message.Write((byte)decal.SpriteIndex); float normalizedXPos = MathHelper.Clamp(MathUtils.InverseLerp(0.0f, rect.Width, decal.CenterPosition.X), 0.0f, 1.0f); float normalizedYPos = MathHelper.Clamp(MathUtils.InverseLerp(-rect.Height, 0.0f, decal.CenterPosition.Y), 0.0f, 1.0f); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 16829a04f..edd4690ca 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -20,12 +20,13 @@ namespace Barotrauma.Networking OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = Order.OrderTargetType.Entity; int? wallSectionIndex = null; + Order order = null; if (type == ChatMessageType.Order) { var orderMessageInfo = OrderChatMessage.ReadOrder(msg); - if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) + if (orderMessageInfo.OrderIdentifier == Identifier.Empty) { - DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderMessageInfo.OrderIndex})."); + DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order identifier is empty."); if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } @@ -34,14 +35,29 @@ namespace Barotrauma.Networking orderTargetPosition = orderMessageInfo.TargetPosition; orderTargetType = orderMessageInfo.TargetType; wallSectionIndex = orderMessageInfo.WallSectionIndex; - var orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; - string orderOption = orderMessageInfo.OrderOption ?? - (orderMessageInfo.OrderOptionIndex == null || orderMessageInfo.OrderOptionIndex < 0 || orderMessageInfo.OrderOptionIndex >= orderPrefab.Options.Length ? - "" : orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value]); - orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character, isNewOrder: orderMessageInfo.IsNewOrder) + var orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; + Identifier orderOption = orderMessageInfo.OrderOption; + if (orderOption.IsEmpty) { - WallSectionIndex = wallSectionIndex - }; + orderOption = orderMessageInfo.OrderOptionIndex == null || orderMessageInfo.OrderOptionIndex < 0 || orderMessageInfo.OrderOptionIndex >= orderPrefab.Options.Length ? + Identifier.Empty : orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value]; + } + if (orderTargetType == Order.OrderTargetType.Position) + { + order = new Order(orderPrefab, orderOption, orderTargetPosition, orderGiver: c.Character) + .WithManualPriority(orderMessageInfo.Priority); + } + else if (orderTargetType == Order.OrderTargetType.WallSection) + { + order = new Order(orderPrefab, orderOption, orderTargetEntity as Structure, wallSectionIndex, orderGiver: c.Character) + .WithManualPriority(orderMessageInfo.Priority); + } + else + { + order = new Order(orderPrefab, orderOption, orderTargetEntity, orderPrefab.GetTargetItemComponent(orderTargetEntity as Item), orderGiver: c.Character) + .WithManualPriority(orderMessageInfo.Priority); + } + orderMsg = new OrderChatMessage(order, orderTargetCharacter, c.Character); txt = orderMsg.Text; } else @@ -95,11 +111,11 @@ namespace Barotrauma.Networking if (c.ChatSpamCount > 3) { //kick for spamming too much - GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked")); + GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked").Value); } else { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null); + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); c.ChatSpamTimer = 10.0f; GameMain.Server.SendDirectChatMessage(denyMsg, c); } @@ -110,7 +126,7 @@ namespace Barotrauma.Networking if (c.ChatSpamTimer > 0.0f && !isOwner) { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null); + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); c.ChatSpamTimer = 10.0f; GameMain.Server.SendDirectChatMessage(denyMsg, c); return; @@ -123,16 +139,6 @@ namespace Barotrauma.Networking { HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order); } - Order order = orderTargetType switch - { - Order.OrderTargetType.Entity => - new Order(orderMsg.Order, orderTargetEntity, orderMsg.Order?.GetTargetItemComponent(orderTargetEntity as Item), orderGiver: orderMsg.Sender), - Order.OrderTargetType.Position => - new Order(orderMsg.Order, orderTargetPosition, orderGiver: orderMsg.Sender), - Order.OrderTargetType.WallSection when orderTargetEntity is Structure s && wallSectionIndex.HasValue => - new Order(orderMsg.Order, s, wallSectionIndex, orderGiver: orderMsg.Sender), - _ => throw new NotImplementedException() - }; if (order != null) { if (order.TargetAllCharacters) @@ -159,7 +165,7 @@ namespace Barotrauma.Networking } else if (orderTargetCharacter != null) { - orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.OrderPriority, orderMsg.Sender); + orderTargetCharacter.SetOrder(order); } } GameMain.Server.SendOrderChatMessage(orderMsg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 86c7ac808..b9daadc66 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -57,8 +57,8 @@ namespace Barotrauma.Networking public bool ReadyToStart; - public List> JobPreferences; - public Pair AssignedJob; + public List JobPreferences; + public JobVariant AssignedJob; public float DeleteDisconnectedTimer; @@ -104,7 +104,7 @@ namespace Barotrauma.Networking partial void InitProjSpecific() { - JobPreferences = new List>(); + JobPreferences = new List(); VoipQueue = new VoipQueue(ID, true, true); GameMain.Server.VoipServer.RegisterQueue(VoipQueue); @@ -149,7 +149,7 @@ namespace Barotrauma.Networking foreach (char character in name) { - if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) { return false; } + if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.Start && (int)character <= charRange.End)) { return false; } } return true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs index 9d15c0e82..fc56ade32 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -12,15 +11,21 @@ namespace Barotrauma partial void CreateNetworkEventProjSpecific(Entity entity, bool remove) { - if (GameMain.Server != null && entity != null) + if (GameMain.Server == null || entity == null) { return; } + + GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) }); + if (entity is Character character && character.Info != null) { - GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) }); - } + foreach (var statKey in character.Info.SavedStatValues.Keys) + { + GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.UpdatePermanentStats, statKey }); + } + } } public void ServerWrite(IWriteMessage message, Client client, object[] extraData = null) { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } SpawnOrRemove entities = (SpawnOrRemove)extraData[0]; @@ -31,17 +36,17 @@ namespace Barotrauma } else { - if (entities.Entity is Item) + if (entities.Entity is Item item) { message.Write((byte)SpawnableType.Item); DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - ((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID, entities.OriginalItemContainerIndex); + item.WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID, entities.OriginalItemContainerIndex, entities.OriginalSlotIndex); } - else if (entities.Entity is Character) + else if (entities.Entity is Character character) { message.Write((byte)SpawnableType.Character); DebugConsole.Log("Writing character spawn data: " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - ((Character)entities.Entity).WriteSpawnData(message, entities.OriginalID, restrictMessageSize: true); + character.WriteSpawnData(message, entities.OriginalID, restrictMessageSize: true); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index e1a4c0d60..4c1b87008 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -36,12 +36,25 @@ namespace Barotrauma.Networking get { return KnownReceivedOffset / (float)Data.Length; } } + private float waitTimer; public float WaitTimer { - get; - set; + get => waitTimer; + set + { + if (value > 0.0f) + { + //setting a wait timer means that network conditions + //aren't ideal, slow down the packet rate + PacketsPerUpdate = Math.Max(PacketsPerUpdate / 2.0f, 1.0f); + } + waitTimer = value; + } } + public const int MaxPacketsPerUpdate = 4; + public float PacketsPerUpdate { get; set; } = 1.0f; + public byte[] Data { get; } public bool Acknowledged; @@ -112,10 +125,7 @@ namespace Barotrauma.Networking public float StallPacketsTime { get; set; } #endif - public List ActiveTransfers - { - get { return activeTransfers; } - } + public IReadOnlyList ActiveTransfers => activeTransfers; public FileSender(ServerPeer serverPeer, int mtu) { @@ -197,9 +207,6 @@ namespace Barotrauma.Networking private void Send(FileTransferOut transfer) { // send another part of the file - long remaining = transfer.Data.Length - transfer.SentOffset; - int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); - IWriteMessage message; try @@ -234,7 +241,7 @@ namespace Barotrauma.Networking transfer.Status = FileTransferStatus.Sending; - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log("Sending file transfer initiation message: "); DebugConsole.Log(" File: " + transfer.FileName); @@ -246,28 +253,44 @@ namespace Barotrauma.Networking return; } - message = new WriteOnlyMessage(); - message.Write((byte)ServerPacketHeader.FILE_TRANSFER); - message.Write((byte)FileTransferMessageType.Data); - - message.Write((byte)transfer.ID); - message.Write(transfer.SentOffset); - - byte[] sendBytes = new byte[sendByteCount]; - Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount); - - message.Write((ushort)sendByteCount); - message.Write(sendBytes, 0, sendByteCount); - - transfer.SentOffset += sendByteCount; - if (transfer.SentOffset > transfer.KnownReceivedOffset + chunkLen * 10 || - transfer.SentOffset >= transfer.Data.Length) + for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++) { - transfer.SentOffset = transfer.KnownReceivedOffset; - transfer.WaitTimer = 0.5f; - } + long remaining = transfer.Data.Length - transfer.SentOffset; + int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); + + message = new WriteOnlyMessage(); + message.Write((byte)ServerPacketHeader.FILE_TRANSFER); + message.Write((byte)FileTransferMessageType.Data); - peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); + message.Write((byte)transfer.ID); + message.Write(transfer.SentOffset); + + message.Write((ushort)sendByteCount); + int chunkDestPos = message.BytePosition; + message.BitPosition += sendByteCount * 8; + message.LengthBits = Math.Max(message.LengthBits, message.BitPosition); + Array.Copy(transfer.Data, transfer.SentOffset, message.Buffer, chunkDestPos, sendByteCount); + + transfer.SentOffset += sendByteCount; + if (transfer.SentOffset >= transfer.Data.Length) + { + transfer.SentOffset = transfer.KnownReceivedOffset; + transfer.WaitTimer = 0.5f; + } + + peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable, compressPastThreshold: false); + + if (GameSettings.CurrentConfig.VerboseLogging) + { + DebugConsole.Log($"Sending {sendByteCount} bytes of the file {transfer.FileName} ({transfer.SentOffset / 1000}/{transfer.Data.Length / 1000} kB sent)"); + } + + //try to increase the packet rate so large files get sent faster, + //this gets reset when packet loss or disorder sets in + transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate, + transfer.PacketsPerUpdate + 0.05f); + } + #if DEBUG transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime); #endif @@ -283,11 +306,6 @@ namespace Barotrauma.Networking transfer.Status = FileTransferStatus.Error; return; } - - if (GameSettings.VerboseLogging) - { - DebugConsole.Log($"Sending {sendByteCount} bytes of the file {transfer.FileName} ({transfer.SentOffset / 1000}/{transfer.Data.Length / 1000} kB sent)"); - } } public void CancelTransfer(FileTransferOut transfer) @@ -302,9 +320,9 @@ namespace Barotrauma.Networking public void ReadFileRequest(IReadMessage inc, Client client) { - byte messageType = inc.ReadByte(); + FileTransferMessageType messageType = (FileTransferMessageType)inc.ReadByte(); - if (messageType == (byte)FileTransferMessageType.Cancel) + if (messageType == FileTransferMessageType.Cancel) { byte transferId = inc.ReadByte(); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); @@ -312,20 +330,28 @@ namespace Barotrauma.Networking return; } - else if (messageType == (byte)FileTransferMessageType.Data) + else if (messageType == FileTransferMessageType.Data) { byte transferId = inc.ReadByte(); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); if (matchingTransfer != null) { matchingTransfer.Acknowledged = true; - int offset = inc.ReadInt32(); - matchingTransfer.KnownReceivedOffset = offset > matchingTransfer.KnownReceivedOffset ? offset : matchingTransfer.KnownReceivedOffset; + int expecting = inc.ReadInt32(); //the offset the client is waiting for + int lastSeen = Math.Min(matchingTransfer.SentOffset, inc.ReadInt32()); //the last offset the client got from us + matchingTransfer.KnownReceivedOffset = Math.Max(expecting, matchingTransfer.KnownReceivedOffset); if (matchingTransfer.SentOffset < matchingTransfer.KnownReceivedOffset) { matchingTransfer.WaitTimer = 0.0f; matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; } + + if (lastSeen - matchingTransfer.KnownReceivedOffset >= chunkLen * 10 || + matchingTransfer.SentOffset >= matchingTransfer.Data.Length) + { + matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; + matchingTransfer.WaitTimer = 0.5f; + } if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) { @@ -334,20 +360,20 @@ namespace Barotrauma.Networking } } - byte fileType = inc.ReadByte(); + FileTransferType fileType = (FileTransferType)inc.ReadByte(); switch (fileType) { - case (byte)FileTransferType.Submarine: + case FileTransferType.Submarine: string fileName = inc.ReadString(); string fileHash = inc.ReadString(); - var requestedSubmarine = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash); + var requestedSubmarine = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.StringRepresentation == fileHash); if (requestedSubmarine != null) { StartTransfer(inc.Sender, FileTransferType.Submarine, requestedSubmarine.FilePath); } break; - case (byte)FileTransferType.CampaignSave: + case FileTransferType.CampaignSave: if (GameMain.GameSession != null && !ActiveTransfers.Any(t => t.Connection == inc.Sender && t.FileType == FileTransferType.CampaignSave)) { @@ -357,6 +383,23 @@ namespace Barotrauma.Networking client.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); } } + break; + case FileTransferType.Mod: + string modName = inc.ReadString(); + Md5Hash modHash = Md5Hash.StringAsHash(inc.ReadString()); + + if (!GameMain.Server.ServerSettings.AllowModDownloads) { return; } + if (!(GameMain.Server.ModSender is { Ready: true })) { return; } + + ContentPackage mod = ContentPackageManager.AllPackages.FirstOrDefault(p => p.Hash.Equals(modHash)); + + if (mod is null) { return; } + + string modCompressedPath = ModSender.GetCompressedModPath(mod); + if (!File.Exists(modCompressedPath)) { return; } + + StartTransfer(inc.Sender, FileTransferType.Mod, modCompressedPath); + break; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs new file mode 100644 index 000000000..9d32305d1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Barotrauma.Networking +{ + class ModSender : IDisposable + { + public const string UploadFolder = "TempMods_Upload"; + public const string Extension = ".barodir.gz"; + + public bool Ready { get; private set; } = false; + + public ModSender() + { + DeleteDir(); + Directory.CreateDirectory(UploadFolder); + TaskPool.Add( + "ModSender", + Task.WhenAll( + ContentPackageManager.EnabledPackages.All + .Where(p => p != ContentPackageManager.VanillaCorePackage && p.HasMultiplayerIncompatibleContent) + .Select(CompressMod)), + (t) => Ready = true); + } + + public static string GetCompressedModPath(ContentPackage mod) + { + string dir = mod.Dir; + string resultFileName = dir.Replace('\\', '_').Replace('/', '_'); + resultFileName = $"{resultFileName}{Extension}"; + return Path.Combine(UploadFolder, resultFileName); + } + + public async Task CompressMod(ContentPackage mod) + { + await Task.Yield(); + string dir = mod.Dir; + SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod), fileName => { }); + } + + private void DeleteDir() + { + if (Directory.Exists(UploadFolder)) { Directory.Delete(UploadFolder, recursive: true); } + } + + public bool IsDisposed { get; private set; } = false; + public void Dispose() + { + IsDisposed = true; + DeleteDir(); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d2ff2efe6..ebd958713 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -7,6 +7,7 @@ using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading; @@ -16,13 +17,9 @@ namespace Barotrauma.Networking { partial class GameServer : NetworkMember { - public override bool IsServer - { - get { return true; } - } + public override bool IsServer => true; private string serverName; - public string ServerName { get { return serverName; } @@ -74,16 +71,14 @@ namespace Barotrauma.Networking private readonly ServerEntityEventManager entityEventManager; - private FileSender fileSender; + public FileSender FileSender { get; private set; } - public FileSender FileSender - { - get { return fileSender; } - } + public ModSender ModSender { get; private set; } + #if DEBUG public void PrintSenderTransters() { - foreach (var transfer in fileSender.ActiveTransfers) + foreach (var transfer in FileSender.ActiveTransfers) { DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString()); } @@ -167,9 +162,11 @@ namespace Barotrauma.Networking serverPeer.OnShutdown = GameMain.Instance.CloseServer; serverPeer.OnOwnerDetermined = OnOwnerDetermined; - fileSender = new FileSender(serverPeer, MsgConstants.MTU); - fileSender.OnEnded += FileTransferChanged; - fileSender.OnStarted += FileTransferChanged; + FileSender = new FileSender(serverPeer, MsgConstants.MTU); + FileSender.OnEnded += FileTransferChanged; + FileSender.OnStarted += FileTransferChanged; + + if (serverSettings.AllowModDownloads) { ModSender = new ModSender(); } serverPeer.Start(); @@ -344,7 +341,7 @@ namespace Barotrauma.Networking base.Update(deltaTime); - fileSender.Update(deltaTime); + FileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); UpdatePing(); @@ -455,7 +452,7 @@ namespace Barotrauma.Networking #if !DEBUG if (endRoundTimer <= 0.0f) { - SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60"), ChatMessageType.Server); + SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60").Value, ChatMessageType.Server); } endRoundDelay = 60.0f; endRoundTimer += deltaTime; @@ -645,10 +642,10 @@ namespace Barotrauma.Networking if (registeredToMaster && (DateTime.Now > refreshMasterTimer || serverSettings.ServerDetailsChanged)) { - if (GameMain.Config.UseSteamMatchmaking) + if (GameSettings.CurrentConfig.UseSteamMatchmaking) { bool refreshSuccessful = SteamManager.RefreshServerDetails(this); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { Log(refreshSuccessful ? "Refreshed server info on the server list." : @@ -668,7 +665,7 @@ namespace Barotrauma.Networking if (Timing.TotalTime > lastPingTime + 1.0) { lastPingData ??= new byte[64]; - for (int i=0;i s.Name == subName && s.MD5Hash.Hash == subHash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); if (gameStarted) { - SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning"), connectedClient, ChatMessageType.MessageBox); + SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; } if (matchingSub == null) { SendDirectChatMessage( - TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName), + TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName).Value, connectedClient, ChatMessageType.MessageBox); } else @@ -787,7 +784,7 @@ namespace Barotrauma.Networking string saveName = inc.ReadString(); if (gameStarted) { - SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning"), connectedClient, ChatMessageType.MessageBox); + SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; } if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.LoadCampaign(saveName); } @@ -829,7 +826,7 @@ namespace Barotrauma.Networking case ClientPacketHeader.FILE_REQUEST: if (serverSettings.AllowFileTransfers) { - fileSender.ReadFileRequest(inc, connectedClient); + FileSender.ReadFileRequest(inc, connectedClient); } break; case ClientPacketHeader.EVENTMANAGER_RESPONSE: @@ -881,12 +878,15 @@ namespace Barotrauma.Networking { errorStr = errorStrNoName = $"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } - var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); - if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) + if (gameStarted) { - string subErrorStr = $" Loaded submarines don't match (client: {string.Join(", ", subNames)}, server: {string.Join(", ", serverSubNames)})."; - errorStr += subErrorStr; - errorStrNoName += subErrorStr; + var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); + if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) + { + string subErrorStr = $" Loaded submarines don't match (client: {string.Join(", ", subNames)}, server: {string.Join(", ", serverSubNames)})."; + errorStr += subErrorStr; + errorStrNoName += subErrorStr; + } } break; } @@ -922,7 +922,7 @@ namespace Barotrauma.Networking Directory.CreateDirectory(ServerLog.SavePath); } - string filePath = "event_error_log_server_" + client.Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + string filePath = $"event_error_log_server_{client.Name}_{DateTime.UtcNow.ToShortTimeString()}.log"; filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (File.Exists(filePath)) { return; } @@ -933,7 +933,7 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode != null) { - errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name); + errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name.Value); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { errorLines.Add("Campaign ID: " + campaign.CampaignID); @@ -957,19 +957,18 @@ namespace Barotrauma.Networking errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); - foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate) + foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) { - errorLines.Add(" " + e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate); } errorLines.Add("Entity IDs:"); - List sortedEntities = Entity.GetEntities().ToList(); - sortedEntities.Sort((e1, e2) => e1.ID.CompareTo(e2.ID)); + Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray(); foreach (Entity e in sortedEntities) { - errorLines.Add(e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add(""); @@ -1160,7 +1159,7 @@ namespace Barotrauma.Networking { c.LastRecvChatMsgID = lastRecvChatMsgID; } - else if (lastRecvChatMsgID != c.LastRecvChatMsgID && GameSettings.VerboseLogging) + else if (lastRecvChatMsgID != c.LastRecvChatMsgID && GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError( "Invalid lastRecvChatMsgID " + lastRecvChatMsgID + @@ -1179,8 +1178,13 @@ namespace Barotrauma.Networking } c.LastRecvEntityEventID = lastRecvEntityEventID; + #warning TODO: remove this later + /*if (!CoroutineManager.IsCoroutineRunning("RoundRestartLoop")) + { + CoroutineManager.StartCoroutine(RoundRestartLoop(), "RoundRestartLoop"); + }*/ } - else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.VerboseLogging) + else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError( "Invalid lastRecvEntityEventID " + lastRecvEntityEventID + @@ -1224,6 +1228,16 @@ namespace Barotrauma.Networking } } + #warning TODO: remove this later + /*private IEnumerable RoundRestartLoop() + { + yield return new WaitForSeconds(8.0f); + EndGame(); + yield return new WaitForSeconds(8.0f); + StartGame(); + yield return CoroutineStatus.Success; + }*/ + private void ReadCrewMessage(IReadMessage inc, Client sender) { if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) @@ -1304,7 +1318,7 @@ namespace Barotrauma.Networking } else { - SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={kickedName}"), sender, ChatMessageType.Console); + SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={kickedName}").Value, sender, ChatMessageType.Console); } break; case ClientPermissions.Ban: @@ -1332,7 +1346,7 @@ namespace Barotrauma.Networking } else { - SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}"), sender, ChatMessageType.Console); + SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}").Value, sender, ChatMessageType.Console); } } break; @@ -1433,9 +1447,9 @@ namespace Barotrauma.Networking case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex; - Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name, ServerLog.MessageType.ServerMessage); + Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name.Value, ServerLog.MessageType.ServerMessage); - if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) + if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier == "multiplayercampaign") { string[] saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false).ToArray(); for (int i = 0; i < saveFiles.Length; i++) @@ -1446,9 +1460,9 @@ namespace Barotrauma.Networking saveFiles[i] = string.Join(";", saveFiles[i].Replace(';', ' '), - doc.Root.GetAttributeString("submarine", ""), - doc.Root.GetAttributeString("savetime", ""), - doc.Root.GetAttributeString("selectedcontentpackages", "")); + doc.Root.GetAttributeStringUnrestricted("submarine", ""), + doc.Root.GetAttributeStringUnrestricted("savetime", ""), + doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", "")); } } @@ -1543,9 +1557,9 @@ namespace Barotrauma.Networking } } - if (!fileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) + if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { - fileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); + FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); c.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)NetTime.Now); } } @@ -1556,7 +1570,7 @@ namespace Barotrauma.Networking /// private void ClientWriteInitial(Client c, IWriteMessage outmsg) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); } @@ -1701,15 +1715,15 @@ namespace Barotrauma.Networking { var entity = c.PendingPositionUpdates.Peek(); if (entity == null || entity.Removed || - (entity is Item item && item.PositionUpdateInterval == float.PositiveInfinity)) + (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) { c.PendingPositionUpdates.Dequeue(); continue; } IWriteMessage tempBuffer = new ReadWriteMessage(); - tempBuffer.Write((byte)ServerNetObject.ENTITY_POSITION); - tempBuffer.Write(entity is Item); + tempBuffer.Write(entity is Item); tempBuffer.WritePadBits(); + tempBuffer.Write(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); if (entity is Item) { ((Item)entity).ServerWritePosition(tempBuffer, c); @@ -1725,6 +1739,8 @@ namespace Barotrauma.Networking break; } + outmsg.Write((byte)ServerNetObject.ENTITY_POSITION); + outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly outmsg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); @@ -1805,7 +1821,7 @@ namespace Barotrauma.Networking outmsg.Write(client.SteamID); outmsg.Write(client.NameID); outmsg.Write(client.Name); - outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : (client.PreferredJob ?? "")); + outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : client.PreferredJob); outmsg.Write((byte)client.PreferredTeam); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); if (c.HasPermission(ClientPermissions.ServerLog)) @@ -1953,7 +1969,7 @@ namespace Barotrauma.Networking #if DEBUG || UNSTABLE DebugConsole.ThrowError(warningMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } #endif GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); } @@ -2043,11 +2059,11 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.QUERY_STARTGAME); msg.Write(selectedSub.Name); - msg.Write(selectedSub.MD5Hash.Hash); + msg.Write(selectedSub.MD5Hash.StringRepresentation); msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); msg.Write(selectedShuttle.Name); - msg.Write(selectedShuttle.MD5Hash.Hash); + msg.Write(selectedShuttle.MD5Hash.StringRepresentation); var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; msg.Write(campaign == null ? (byte)0 : campaign.CampaignID); @@ -2069,10 +2085,10 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; } - if (fileSender.ActiveTransfers.Count > 0) + if (FileSender.ActiveTransfers.Count > 0) { float waitForTransfersTimer = 20.0f; - while (fileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) + while (FileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) { waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime; yield return CoroutineStatus.Running; @@ -2156,7 +2172,7 @@ namespace Barotrauma.Networking GameMain.GameSession.StartRound(campaign.NextLevel, mirrorLevel: campaign.MirrorLevel); SubmarineSwitchLoad = false; campaign.AssignClientCharacterInfos(connectedClients); - Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); + Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + campaign.NextLevel.Seed, ServerLog.MessageType.ServerMessage); } @@ -2164,14 +2180,14 @@ namespace Barotrauma.Networking { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); - Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); + Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); } foreach (Mission mission in GameMain.GameSession.Missions) { - Log("Mission: " + mission.Prefab.Name, ServerLog.MessageType.ServerMessage); + Log("Mission: " + mission.Prefab.Name.Value, ServerLog.MessageType.ServerMessage); } if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted) @@ -2256,9 +2272,9 @@ namespace Barotrauma.Networking client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name); } characterInfos.Add(client.CharacterInfo); - if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) + if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.Prefab) { - client.CharacterInfo.Job = new Job(client.AssignedJob.First, Rand.RandSync.Unsynced, client.AssignedJob.Second); + client.CharacterInfo.Job = new Job(client.AssignedJob.Prefab, Rand.RandSync.Unsynced, client.AssignedJob.Variant); } } @@ -2305,7 +2321,7 @@ namespace Barotrauma.Networking wp.SpawnType == SpawnType.Human && wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull?.OutpostModuleTags != null && - wp.CurrentHull.OutpostModuleTags.Contains("airlock")); + wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); @@ -2481,14 +2497,14 @@ namespace Barotrauma.Networking msg.Write(levelSeed); msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write(gameSession.SubmarineInfo.Name); - msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); + msg.Write(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; msg.Write(selectedShuttle.Name); - msg.Write(selectedShuttle.MD5Hash.Hash); + msg.Write(selectedShuttle.MD5Hash.StringRepresentation); msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); foreach (Mission mission in GameMain.GameSession.GameMode.Missions) { - msg.Write((short)MissionPrefab.List.IndexOf(mission.Prefab)); + msg.Write(mission.Prefab.UintIdentifier); } } else @@ -2526,8 +2542,7 @@ namespace Barotrauma.Networking msg.Write((ushort)contentToPreload.Count()); foreach (ContentFile contentFile in contentToPreload) { - msg.Write((byte)contentFile.Type); - msg.Write(contentFile.Path); + msg.Write(contentFile.Path.Value); } msg.Write(Submarine.MainSub?.Info.EqualityCheckVal ?? 0); msg.Write((byte)GameMain.GameSession.Missions.Count()); @@ -2555,7 +2570,7 @@ namespace Barotrauma.Networking return; } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage); @@ -2664,7 +2679,7 @@ namespace Barotrauma.Networking { UInt16 nameId = inc.ReadUInt16(); string newName = inc.ReadString(); - string newJob = inc.ReadString(); + Identifier newJob = inc.ReadIdentifier(); CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } @@ -3150,7 +3165,7 @@ namespace Barotrauma.Networking if (type.Value != ChatMessageType.MessageBox) { - string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message) : message; + string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message).Value : message; if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderClient, senderCharacter); @@ -3169,11 +3184,11 @@ namespace Barotrauma.Networking //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + AddChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); } } @@ -3355,9 +3370,8 @@ namespace Barotrauma.Networking serverPeer.Send(msg, recipient.Connection, DeliveryMethod.Reliable); } - public void GiveAchievement(Character character, string achievementIdentifier) + public void GiveAchievement(Character character, Identifier achievementIdentifier) { - achievementIdentifier = achievementIdentifier.ToLowerInvariant(); foreach (Client client in connectedClients) { if (client.Character == character) @@ -3368,9 +3382,8 @@ namespace Barotrauma.Networking } } - public void IncrementStat(Character character, string achievementIdentifier, int amount) + public void IncrementStat(Character character, Identifier achievementIdentifier, int amount) { - achievementIdentifier = achievementIdentifier.ToLowerInvariant(); foreach (Client client in connectedClients) { if (client.Character == character) @@ -3381,7 +3394,7 @@ namespace Barotrauma.Networking } } - public void GiveAchievement(Client client, string achievementIdentifier) + public void GiveAchievement(Client client, Identifier achievementIdentifier) { if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } client.GivenAchievements.Add(achievementIdentifier); @@ -3394,7 +3407,7 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void IncrementStat(Client client, string achievementIdentifier, int amount) + public void IncrementStat(Client client, Identifier achievementIdentifier, int amount) { if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } @@ -3406,13 +3419,13 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void SendTraitorMessage(Client client, string message, string missionIdentifier, TraitorMessageType messageType) + public void SendTraitorMessage(Client client, string message, Identifier missionIdentifier, TraitorMessageType messageType) { if (client == null) { return; } var msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.TRAITOR_MESSAGE); msg.Write((byte)messageType); - msg.Write(missionIdentifier ?? ""); + msg.Write(missionIdentifier); msg.Write(message); serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } @@ -3484,18 +3497,11 @@ namespace Barotrauma.Networking return; } - Gender gender = Gender.Male; - Race race = Race.White; - int headSpriteId = 0; - try + int tagCount = message.ReadByte(); + HashSet tagSet = new HashSet(); + for (int i = 0; i < tagCount; i++) { - gender = (Gender)message.ReadByte(); - race = (Race)message.ReadByte(); - headSpriteId = message.ReadByte(); - } - catch (Exception e) - { - DebugConsole.Log("Received invalid characterinfo from \"" + sender.Name + "\"! { " + e.Message + " }"); + tagSet.Add(message.ReadIdentifier()); } int hairIndex = message.ReadByte(); int beardIndex = message.ReadByte(); @@ -3505,7 +3511,7 @@ namespace Barotrauma.Networking Color hairColor = message.ReadColorR8G8B8(); Color facialHairColor = message.ReadColorR8G8B8(); - List> jobPreferences = new List>(); + List jobPreferences = new List(); int count = message.ReadByte(); // TODO: modding support? for (int i = 0; i < Math.Min(count, 3); i++) @@ -3514,15 +3520,15 @@ namespace Barotrauma.Networking int variant = message.ReadByte(); if (JobPrefab.Prefabs.ContainsKey(jobIdentifier)) { - jobPreferences.Add(new Pair(JobPrefab.Prefabs[jobIdentifier], variant)); + jobPreferences.Add(new JobVariant(JobPrefab.Prefabs[jobIdentifier], variant)); } } sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, sender.Name); - sender.CharacterInfo.RecreateHead(headSpriteId, race, gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); - sender.CharacterInfo.SkinColor = skinColor; - sender.CharacterInfo.HairColor = hairColor; - sender.CharacterInfo.FacialHairColor = facialHairColor; + sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + sender.CharacterInfo.Head.SkinColor = skinColor; + sender.CharacterInfo.Head.HairColor = hairColor; + sender.CharacterInfo.Head.FacialHairColor = facialHairColor; if (jobPreferences.Count > 0) { @@ -3556,7 +3562,7 @@ namespace Barotrauma.Networking foreach (KeyValuePair clientJob in campaignAssigned) { assignedClientCount[clientJob.Value.Prefab]++; - clientJob.Key.AssignedJob = new Pair(clientJob.Value.Prefab, clientJob.Value.Variant); + clientJob.Key.AssignedJob = new JobVariant(clientJob.Value.Prefab, clientJob.Value.Variant); } } @@ -3574,7 +3580,7 @@ namespace Barotrauma.Networking for (int i = unassigned.Count - 1; i >= 0; i--) { if (unassigned[i].JobPreferences.Count == 0) { continue; } - if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].First.AllowAlways) { continue; } + if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].Prefab.AllowAlways) { continue; } unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; unassigned.RemoveAt(i); } @@ -3611,8 +3617,8 @@ namespace Barotrauma.Networking void AssignJob(Client client, JobPrefab jobPrefab) { client.AssignedJob = - client.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? - new Pair(jobPrefab, Rand.Int(jobPrefab.Variants)); + client.JobPreferences.FirstOrDefault(jp => jp.Prefab == jobPrefab) ?? + new JobVariant(jobPrefab, Rand.Int(jobPrefab.Variants)); assignedClientCount[jobPrefab]++; unassigned.Remove(client); @@ -3657,7 +3663,7 @@ namespace Barotrauma.Networking Client client = unassigned[i]; if (preferenceIndex >= client.JobPreferences.Count) { continue; } var preferredJob = client.JobPreferences[preferenceIndex]; - JobPrefab jobPrefab = preferredJob.First; + JobPrefab jobPrefab = preferredJob.Prefab; if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber || client.Karma < jobPrefab.MinKarma) { //can't assign this job if maximum number has reached or the clien't karma is too low @@ -3690,24 +3696,24 @@ namespace Barotrauma.Networking if (skips >= jobList.Count) { break; } } c.AssignedJob = - c.JobPreferences.FirstOrDefault(jp => jp.First == jobList[jobIndex]) ?? - new Pair(jobList[jobIndex], 0); - assignedClientCount[c.AssignedJob.First]++; + c.JobPreferences.FirstOrDefault(jp => jp.Prefab == jobList[jobIndex]) ?? + new JobVariant(jobList[jobIndex], 0); + assignedClientCount[c.AssignedJob.Prefab]++; } //if one of the client's preferences is still available, give them that job - else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.First))) + else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.Prefab))) { - foreach (Pair preferredJob in c.JobPreferences) + foreach (JobVariant preferredJob in c.JobPreferences) { c.AssignedJob = preferredJob; - assignedClientCount[preferredJob.First]++; + assignedClientCount[preferredJob.Prefab]++; break; } } else //none of the client's preferred jobs available, choose a random job { - c.AssignedJob = new Pair(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); - assignedClientCount[c.AssignedJob.First]++; + c.AssignedJob = new JobVariant(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); + assignedClientCount[c.AssignedJob.Prefab]++; } } } @@ -3751,11 +3757,11 @@ namespace Barotrauma.Networking { if (unassignedBots.Count == 0) { break; } - JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandom(); + JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandomUnsynced(); if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; } - var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.Server); - unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.Server, variant); + var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.ServerAndClient); + unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.ServerAndClient, variant); assignedPlayerCount[jobPrefab]++; unassignedBots.Remove(unassignedBots[0]); canAssign = true; @@ -3768,15 +3774,16 @@ namespace Barotrauma.Networking //find all jobs that are still available var remainingJobs = JobPrefab.Prefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber); //all jobs taken, give a random job - if (remainingJobs.Count() == 0) + if (remainingJobs.None()) { DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); - c.Job = Job.Random(); + #warning TODO: is this randsync correct? + c.Job = Job.Random(Rand.RandSync.ServerAndClient); assignedPlayerCount[c.Job.Prefab]++; } else //some jobs still left, choose one of them by random { - var job = remainingJobs.GetRandom(); + var job = remainingJobs.GetRandomUnsynced(); var variant = Rand.Range(0, job.Variants); c.Job = new Job(job, Rand.RandSync.Unsynced, variant); assignedPlayerCount[c.Job.Prefab]++; @@ -3791,7 +3798,7 @@ namespace Barotrauma.Networking foreach (Client c in clients) { if (ServerSettings.KarmaEnabled && c.Karma < job.MinKarma) { continue; } - int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.First == job)); + int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.Prefab == job)); if (index > -1 && index < bestPreference) { bestPreference = index; @@ -3867,6 +3874,8 @@ namespace Barotrauma.Networking serverSettings.SaveSettings(); + ModSender.Dispose(); + if (serverSettings.SaveServerLogs) { Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index a0bab780e..8fe44eb71 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -128,7 +128,7 @@ namespace Barotrauma clientMemory.PreviousNotifiedKarma >= KickBanThreshold + KarmaNotificationInterval && client.Karma < KickBanThreshold + KarmaNotificationInterval) { - GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning"), client); + GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning").Value, client); GameServer.Log(GameServer.ClientLogName(client) + " has been warned for having dangerously low karma.", ServerLog.MessageType.Karma); clientMemory.PreviousNotifiedKarma = client.Karma; clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime; @@ -170,7 +170,7 @@ namespace Barotrauma existingAffliction.Strength = herpesStrength; if (herpesStrength <= 0.0f) { - client.Character.CharacterHealth.ReduceAffliction(null, "invertcontrols", 100.0f); + client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100.0f); } } @@ -283,7 +283,7 @@ namespace Barotrauma if (foundItem == null) { return; } - bool isIdCard = foundItem.prefab.Identifier == "idcard"; + bool isIdCard = ((MapEntity)foundItem).Prefab.Identifier == "idcard"; bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; if (isIdCard) @@ -394,8 +394,8 @@ namespace Barotrauma } //attacking/healing clowns has a smaller effect on karma - if (target.HasEquippedItem("clownmask") && - target.HasEquippedItem("clowncostume")) + if (target.HasEquippedItem("clownmask".ToIdentifier()) && + target.HasEquippedItem("clowncostume".ToIdentifier())) { damage *= 0.5f; stun *= 0.5f; @@ -604,8 +604,8 @@ namespace Barotrauma if (client == null) { return; } //all penalties/rewards are halved when wearing a clown costume - if (target.HasEquippedItem("clownmask") && - target.HasEquippedItem("clowncostume")) + if (target.HasEquippedItem("clownmask".ToIdentifier()) && + target.HasEquippedItem("clowncostume".ToIdentifier())) { amount *= 0.5f; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index d50e6209f..b11c6bcf4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -179,7 +179,7 @@ namespace Barotrauma.Networking catch (Exception e) { string entityName = bufferedEvent.TargetEntity == null ? "null" : bufferedEvent.TargetEntity.ToString(); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { string errorMsg = "Failed to read server event for entity \"" + entityName + "\"!"; GameServer.Log(errorMsg + "\n" + e.StackTrace.CleanupStackTrace(), ServerLog.MessageType.Error); @@ -347,7 +347,7 @@ namespace Barotrauma.Networking count++; if (count > 3) { break; } } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { GameServer.Log(warningMsg, ServerLog.MessageType.Error); } @@ -482,7 +482,7 @@ namespace Barotrauma.Networking //skip the event if we've already received it if (thisEventID != (UInt16)(sender.LastSentEntityEventID + 1)) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Received msg " + thisEventID + ", expecting " + sender.LastSentEntityEventID, Color.Red); } @@ -493,7 +493,7 @@ namespace Barotrauma.Networking //entity not found -> consider the even read and skip over it //(can happen, for example, when a client uses a medical item repeatedly //and creates an event for it before receiving the event about it being removed) - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage( "Received msg " + thisEventID + ", entity " + entityID + " not found", @@ -504,7 +504,7 @@ namespace Barotrauma.Networking } else { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Received msg " + thisEventID, Microsoft.Xna.Framework.Color.Green); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 629a767cf..16cfd6683 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -129,7 +129,7 @@ namespace Barotrauma.Networking #if DEBUG DebugConsole.ThrowError(errorMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif } @@ -326,7 +326,7 @@ namespace Barotrauma.Networking } } - public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (netServer == null) { return; } @@ -353,7 +353,7 @@ namespace Barotrauma.Networking NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); lidgrenMsg.Write((UInt16)length); lidgrenMsg.Write(msgData, 0, length); @@ -422,7 +422,7 @@ namespace Barotrauma.Networking { if (pendingClient.SteamID == null) { - bool requireSteamAuth = GameMain.Config.RequireSteamAuthentication; + bool requireSteamAuth = GameSettings.CurrentConfig.RequireSteamAuthentication; #if DEBUG requireSteamAuth = false; #endif diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 546ba9543..1e2d3e6d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -123,7 +123,7 @@ namespace Barotrauma.Networking return; } - string language = inc.ReadString(); + LanguageIdentifier language = inc.ReadIdentifier().ToLanguageIdentifier(); pendingClient.Connection.Language = language; Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); @@ -246,12 +246,14 @@ namespace Barotrauma.Networking case ConnectionInitialization.ContentPackageOrder: outMsg.Write(GameMain.Server.ServerName); - var mpContentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); + var mpContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); for (int i = 0; i < mpContentPackages.Count; i++) { outMsg.Write(mpContentPackages[i].Name); - outMsg.Write(mpContentPackages[i].MD5hash.Hash); + byte[] hashBytes = mpContentPackages[i].Hash.ByteRepresentation; + outMsg.WriteVariableUInt32((UInt32)hashBytes.Length); + outMsg.Write(hashBytes, 0, hashBytes.Length); outMsg.Write(mpContentPackages[i].SteamWorkshopId); UInt32 installTimeDiffSeconds = (UInt32)((mpContentPackages[i].InstallTime ?? DateTime.UtcNow) - DateTime.UtcNow).TotalSeconds; outMsg.Write(installTimeDiffSeconds); @@ -294,7 +296,7 @@ namespace Barotrauma.Networking } } - public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod); + public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void Disconnect(NetworkConnection conn, string msg = null); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 6af0c5221..2d1cb634c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Networking #if DEBUG DebugConsole.ThrowError(errorMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif } @@ -223,7 +223,7 @@ namespace Barotrauma.Networking string ownerName = inc.ReadString(); OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID) { - Language = GameMain.Config.Language + Language = GameSettings.CurrentConfig.Language }; OwnerConnection.SetOwnerSteamIDIfUnknown(OwnerSteamID); @@ -250,7 +250,7 @@ namespace Barotrauma.Networking throw new InvalidOperationException("Called InitializeSteamServerCallbacks on SteamP2PServerPeer!"); } - public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!started) { return; } @@ -263,7 +263,7 @@ namespace Barotrauma.Networking IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[16]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); msgToSend.Write(conn.SteamID); msgToSend.Write((byte)deliveryMethod); msgToSend.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index d81789f78..38252b58f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -227,7 +227,7 @@ namespace Barotrauma.Networking } var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); - shuttleGaps.ForEach(g => Spawner.AddToRemoveQueue(g)); + shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g)); var dockingPorts = Item.ItemList.FindAll(i => i.Submarine == RespawnShuttle && i.GetComponent() != null); dockingPorts.ForEach(d => d.GetComponent().Undock()); @@ -355,7 +355,7 @@ namespace Barotrauma.Networking { if (campaign?.GetClientCharacterData(c) == null || c.CharacterInfo.Job == null) { - c.CharacterInfo.Job = new Job(c.AssignedJob.First, Rand.RandSync.Unsynced, c.AssignedJob.Second); + c.CharacterInfo.Job = new Job(c.AssignedJob.Prefab, Rand.RandSync.Unsynced, c.AssignedJob.Variant); } } @@ -369,17 +369,17 @@ namespace Barotrauma.Networking if ((shuttlePos != null && Level.Loaded.GetRealWorldDepth(shuttlePos.Value.Y) > Level.DefaultRealWorldCrushDepth) || Level.Loaded.GetRealWorldDepth(Submarine.MainSub.WorldPosition.Y) > Level.DefaultRealWorldCrushDepth) { - divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t.Equals("respawnsuitdeep", StringComparison.OrdinalIgnoreCase))); + divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuitdeep")); } if (divingSuitPrefab == null) { divingSuitPrefab = - ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t.Equals("respawnsuit", StringComparison.OrdinalIgnoreCase))) ?? - ItemPrefab.Find(null, "divingsuit"); + ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuit")) ?? + ItemPrefab.Find(null, "divingsuit".ToIdentifier()); } - ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank"); - ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter"); - ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell"); + ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank".ToIdentifier()); + ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter".ToIdentifier()); + ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell".ToIdentifier()); var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo); @@ -522,7 +522,7 @@ namespace Barotrauma.Networking if (characterInfo?.Job == null) { return; } foreach (Skill skill in characterInfo.Job.Skills) { - var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier.Equals(s.Identifier, StringComparison.OrdinalIgnoreCase)); + var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); if (skillPrefab == null) { continue; } skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.Start, SkillReductionOnCampaignMidroundRespawn); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 9f9e10a7c..334e62414 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -268,7 +268,7 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); - doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => c.First + "-" + c.Second))); + doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => $"{c.Start}-{c.End}"))); SerializableProperty.SerializeProperties(this, doc.Root, true); @@ -307,7 +307,7 @@ namespace Barotrauma.Networking if (string.IsNullOrEmpty(doc.Root.GetAttributeString("losmode", ""))) { - LosMode = GameMain.Config.LosMode; + LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); @@ -370,7 +370,12 @@ namespace Barotrauma.Networking } } - if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Pair(min, max)); } + if (min > max) + { + //swap min and max + (min, max) = (max, min); + } + if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Range(min, max)); } } AllowedRandomMissionTypes = new List(); @@ -399,12 +404,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode); GameMain.NetLobbyScreen.SetBotCount(BotCount); - List monsterNames = CharacterPrefab.Prefabs.Select(p => p.Identifier).ToList(); - MonsterEnabled = new Dictionary(); - foreach (string s in monsterNames) - { - if (!MonsterEnabled.ContainsKey(s)) MonsterEnabled.Add(s, true); - } + MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); } public string SelectNonHiddenSubmarine(string current = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 5868b7235..290833c32 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -85,7 +85,7 @@ namespace Barotrauma string hash = equalityCheckVal > 0 ? string.Empty : inc.ReadString(); SubmarineInfo sub = equalityCheckVal > 0 ? SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.EqualityCheckVal == equalityCheckVal) : - SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.MD5Hash.Hash == hash); + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.MD5Hash.StringRepresentation == hash); sender.SetVote(voteType, sub); break; case VoteType.Mode: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 97461c229..0712c25b6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -108,13 +108,10 @@ namespace Barotrauma sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); sb.AppendLine("\n"); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); - if (GameMain.Config != null) + sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); + if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); - if (GameMain.Config.AllEnabledPackages != null) - { - sb.AppendLine("Selected content packages: " + (!GameMain.Config.AllEnabledPackages.Any() ? "None" : string.Join(", ", GameMain.Config.AllEnabledPackages.Select(c => c.Name)))); - } + sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); @@ -176,7 +173,8 @@ namespace Barotrauma File.WriteAllText(filePath, sb.ToString()); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } if (GameAnalyticsManager.SendUserStatistics) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index cc3baa6d0..aaccf295c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -54,14 +54,14 @@ namespace Barotrauma } } - public string SelectedModeIdentifier + public Identifier SelectedModeIdentifier { get { return GameModes[SelectedModeIndex].Identifier; } set { for (int i = 0; i < GameModes.Length; i++) { - if (GameModes[i].Identifier.ToLower() == value.ToLower()) + if (GameModes[i].Identifier == value) { SelectedModeIndex = i; break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs similarity index 86% rename from Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs rename to Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index beb842ec4..1b2c09f73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -4,13 +4,11 @@ namespace Barotrauma.Steam { partial class SteamManager { - #region Server - - private static void InitializeProjectSpecific() { isInitialized = true; } + private static void InitializeProjectSpecific() { IsInitialized = true; } public static bool CreateServer(Networking.GameServer server, bool isPublic) { - isInitialized = true; + IsInitialized = true; Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma") { @@ -39,26 +37,26 @@ namespace Barotrauma.Steam public static bool RefreshServerDetails(Networking.GameServer server) { - if (!isInitialized || !Steamworks.SteamServer.IsValid) + if (!IsInitialized || !Steamworks.SteamServer.IsValid) { return false; } - var contentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); // These server state variables may be changed at any time. Note that there is no longer a mechanism - // to send the player count. The player count is maintained by steam and you should use the player + // to send the player count. The player count is maintained by Steam and you should use the player // creation/authentication functions to maintain your player count. Steamworks.SteamServer.ServerName = server.ServerName; Steamworks.SteamServer.MaxPlayers = server.ServerSettings.MaxPlayers; Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword; - Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName ?? ""; + Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? ""; Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString()); Steamworks.SteamServer.SetKey("message", GameMain.Server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); Steamworks.SteamServer.SetKey("playercount", GameMain.Server.ConnectedClients.Count.ToString()); Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); - Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash))); + Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); Steamworks.SteamServer.SetKey("usingwhitelist", (server.ServerSettings.Whitelist != null && server.ServerSettings.Whitelist.Enabled).ToString()); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); @@ -68,7 +66,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString()); Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString()); Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); - Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier); + Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); Steamworks.SteamServer.DedicatedServer = true; @@ -78,7 +76,7 @@ namespace Barotrauma.Steam public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) { - if (!isInitialized || !Steamworks.SteamServer.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; + if (!IsInitialized || !Steamworks.SteamServer.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); Steamworks.BeginAuthResult startResult = Steamworks.SteamServer.BeginAuthSession(authTicketData, clientSteamID); @@ -92,7 +90,7 @@ namespace Barotrauma.Steam public static void StopAuthSession(ulong clientSteamID) { - if (!isInitialized || !Steamworks.SteamServer.IsValid) return; + if (!IsInitialized || !Steamworks.SteamServer.IsValid) return; DebugConsole.Log("SteamManager ending auth session with Steam client " + clientSteamID); Steamworks.SteamServer.EndSession(clientSteamID); @@ -100,13 +98,11 @@ namespace Barotrauma.Steam public static bool CloseServer() { - if (!isInitialized || !Steamworks.SteamServer.IsValid) return false; + if (!IsInitialized || !Steamworks.SteamServer.IsValid) return false; Steamworks.SteamServer.Shutdown(); return true; } - - #endregion } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs index 93806e349..37894b947 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs @@ -29,7 +29,8 @@ namespace Barotrauma public virtual IEnumerable CompletedTextKeys => new string[] { }; public virtual IEnumerable CompletedTextValues(Traitor traitor) => new string[] { }; - protected virtual string FormatText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => TextManager.FormatServerMessageWithGenderPronouns(traitor?.Character?.Info?.Gender ?? Gender.None, textId, keys, values); + protected virtual string FormatText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) + => TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, textId, keys.Zip(values, (k,v) => (k,v)).ToArray()); protected internal virtual string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs index 17e4ae552..6fd116862 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs @@ -51,11 +51,11 @@ namespace Barotrauma { continue; } - var identifierMatches = matchIdentifier && item.prefab.Identifier == tag; + var identifierMatches = matchIdentifier && ((MapEntity)item).Prefab.Identifier == tag; if (identifierMatches && tagPrefabName == null) { var textId = item.Prefab.GetItemNameTextId(); - tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name; + tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name.Value; } if (identifierMatches || (matchTag && item.HasTag(tag))) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs index 5bfff56a5..e9977fa73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs @@ -28,7 +28,7 @@ namespace Barotrauma private enum EntityTypes { Character, Item } - private string[] entities; + private Identifier[] entities; private EntityTypes[] entityTypes; public override void Update(float deltaTime) @@ -67,7 +67,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase) && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) + if (character.SpeciesName == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) { activeEntity = character; transformationTime = 0.0; @@ -82,7 +82,7 @@ namespace Barotrauma { continue; } - if (item.prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance) + if (((MapEntity)item).Prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance) { activeEntity = item; transformationTime = 0.0; @@ -117,7 +117,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase)) + if (character.SpeciesName == entities[activeEntityIndex]) { activeEntity = character; break; @@ -131,7 +131,7 @@ namespace Barotrauma { continue; } - if (item.prefab.Identifier.Equals(entities[0], StringComparison.OrdinalIgnoreCase)) + if (((MapEntity)item).Prefab.Identifier == entities[0]) { activeEntity = item; break; @@ -146,7 +146,7 @@ namespace Barotrauma public GoalEntityTransformation(string[] entities, string[] entityTypes, string catalystItemIdentifier) : base() { - this.entities = entities; + this.entities = entities.ToIdentifiers().ToArray(); this.entityTypes = new EntityTypes[entityTypes.Length]; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs index 06f43f91b..57871e224 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs @@ -15,7 +15,7 @@ namespace Barotrauma private readonly bool preferNew; private readonly bool allowNew; private readonly bool allowExisting; - private readonly HashSet allowedContainerIdentifiers = new HashSet(); + private readonly HashSet allowedContainerIdentifiers = new HashSet(); private ItemPrefab targetPrefab; private ItemPrefab containedPrefab; @@ -83,7 +83,7 @@ namespace Barotrauma { continue; } - if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(item.prefab.Identifier)) + if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(((MapEntity)item).Prefab.Identifier)) { if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null)) { @@ -166,7 +166,7 @@ namespace Barotrauma targetPrefabTextId = targetPrefab.GetItemNameTextId(); } - targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name; + targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name.Value; targetContainer = FindTargetContainer(Traitors, targetPrefab); if (targetContainer == null) { @@ -175,9 +175,9 @@ namespace Barotrauma return false; } var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId(); - targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name; - var targetHullTextId = targetContainer.CurrentHull?.prefab.GetHullNameTextId(); - targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName ?? ""; + targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name.Value; + var targetHullTextId = targetContainer.CurrentHull?.Prefab.GetHullNameTextId(); + targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName.Value ?? ""; if (allowNew && !targetContainer.OwnInventory.IsFull()) { existingItems.Clear(); @@ -185,7 +185,7 @@ namespace Barotrauma { existingItems.Add(item); } - Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory, onSpawned: item => + Entity.Spawner.AddItemToSpawnQueue(targetPrefab, targetContainer.OwnInventory, onSpawned: item => { item.AddTag("traitormissionitem"); }); @@ -216,7 +216,7 @@ namespace Barotrauma { for (int i = 0; i < spawnAmount; i++) { - Entity.Spawner.AddToSpawnQueue(containedPrefab, target.OwnInventory); + Entity.Spawner.AddItemToSpawnQueue(containedPrefab, target.OwnInventory); } } existingItems.Clear(); @@ -224,7 +224,7 @@ namespace Barotrauma } } - public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params string[] allowedContainerIdentifiers) + public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params Identifier[] allowedContainerIdentifiers) { this.filter = filter; this.identifier = identifier; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs index cceda84c4..31aad8c96 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs @@ -21,7 +21,7 @@ namespace Barotrauma base.Update(deltaTime); var validHullsCount = 0; var floodingAmount = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine == null || hull.Submarine.Info.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs index 8d4500fa8..32b024770 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs @@ -15,7 +15,7 @@ namespace Barotrauma private bool isCompleted; private const float gracePeriod = 1f; - private string speciesId; + private Identifier speciesId; private string targetCharacterName; private Character targetCharacter; private float timer; @@ -52,7 +52,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.Equals(speciesId, StringComparison.OrdinalIgnoreCase)) + if (character.SpeciesName == speciesId) { targetCharacter = character; break; @@ -64,9 +64,9 @@ namespace Barotrauma return targetCharacter != null; } - public GoalKeepTransformedAlive(string speciesId) : base() + public GoalKeepTransformedAlive(Identifier speciesId) : base() { - this.speciesId = speciesId.ToLowerInvariant(); + this.speciesId = speciesId; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs index ca486e419..6cb4c70e6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs @@ -13,12 +13,12 @@ namespace Barotrauma public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[causeofdeath]", "[targethullname]" }); public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] - { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath(), targetHull != null ? TextManager.Get($"roomname.{targetHull}") : string.Empty }); + { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath().Value, targetHull != null ? TextManager.Get($"roomname.{targetHull}").Value : string.Empty }); private bool isCompleted = false; public override bool IsCompleted => isCompleted; - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); + public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); private CauseOfDeathType requiredCauseOfDeath; private string afflictionId; @@ -102,7 +102,7 @@ namespace Barotrauma return true; } - private string GetCauseOfDeath() + private LocalizedString GetCauseOfDeath() { if (requiredCauseOfDeath != CauseOfDeathType.Affliction || afflictionId == string.Empty) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs index 3ffb3953c..fe4edd2d4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs @@ -8,8 +8,8 @@ namespace Barotrauma { public class GoalReplaceInventory : HumanoidGoal { - private readonly HashSet sabotageContainerIds = new HashSet(); - private readonly HashSet validReplacementIds = new HashSet(); + private readonly HashSet sabotageContainerIds = new HashSet(); + private readonly HashSet validReplacementIds = new HashSet(); private readonly float replaceAmount; @@ -33,7 +33,7 @@ namespace Barotrauma { continue; } - if (sabotageContainerIds.Contains(item.prefab.Identifier)) + if (sabotageContainerIds.Contains(((MapEntity)item).Prefab.Identifier)) { ++totalAmount; if (item.OwnInventory.AllItems.All(containedItem => !validReplacementIds.Contains(containedItem.Prefab.Identifier))) @@ -59,7 +59,7 @@ namespace Barotrauma return true; } - public GoalReplaceInventory(string[] containerIds, string[] replacementIds, float replaceAmount) + public GoalReplaceInventory(Identifier[] containerIds, Identifier[] replacementIds, float replaceAmount) { sabotageContainerIds.UnionWith(containerIds); validReplacementIds.UnionWith(replacementIds); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs index 7c51dc64c..0175429b9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs @@ -37,15 +37,10 @@ namespace Barotrauma targetItems.Add(item); } } - //only target items in the main sub if there are any - if (targetItems.Count > 1 && targetItems.Any(it => it.Submarine == Submarine.MainSub)) - { - targetItems.RemoveAll(it => it.Submarine != Submarine.MainSub); - } if (targetItems.Count > 0) { var textId = targetItems[0].Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name; + targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name.Value; } return targetItems.Count > 0; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs index bcc4717aa..2f1314d49 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs @@ -46,7 +46,7 @@ namespace Barotrauma if (targetConnectionPanels.Count > 0) { var textId = targetConnectionPanels[0].Item.Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name; + targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name.Value; } return targetConnectionPanels.Count > 0; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs index c33841af9..f4b5d894d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma @@ -14,12 +15,13 @@ namespace Barotrauma public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString() }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString(CultureInfo.InvariantCulture) }); protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, new[] { "[infotext]", "[duration]" }, new[] { infoText, requiredDuration.ToString() }) : infoText; + return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, + ("[infotext]", infoText), ("[duration]", requiredDuration.ToString(CultureInfo.InvariantCulture))) : infoText; } private bool isCompleted = false; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs index 48e38f060..f2603e594 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs @@ -17,7 +17,7 @@ namespace Barotrauma protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, new[] { "[infotext]", "[timelimit]" }, new[] { infoText, $"{TimeSpan.FromSeconds(timeLimit):g}" }) : infoText; + return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, ("[infotext]", infoText), ("[timelimit]", $"{TimeSpan.FromSeconds(timeLimit):g}")) : infoText; } public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && (!Traitors.Any(IsStarted) || timeRemaining > 0.0f); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs index e749f0a3f..22b5a0a74 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs @@ -14,7 +14,7 @@ namespace Barotrauma public override IEnumerable StatusTextValues(Traitor traitor) { var values = base.StatusTextValues(traitor).ToArray(); - values[1] = TextManager.GetServerMessage(StatusValueTextId); + values[1] = TextManager.FormatServerMessage(StatusValueTextId); return values; } @@ -24,7 +24,7 @@ namespace Barotrauma protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, new[] { "[infotext]" }, new[] { infoText }) : infoText; + return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, ("[infotext]", infoText)) : infoText; } public GoalIsOptional(Goal goal, string optionalInfoTextId) : base(goal) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs index 522e5d661..2a48fe8ae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs @@ -30,7 +30,7 @@ namespace Barotrauma } public override IEnumerable StatusTextKeys => Goal.StatusTextKeys; - public override IEnumerable StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; + public override IEnumerable StatusTextValues(Traitor traitor) => new string[] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; public override IEnumerable InfoTextKeys => Goal.InfoTextKeys; public override IEnumerable InfoTextValues(Traitor traitor) => Goal.InfoTextValues(traitor); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs index b8eaf65dc..8c09c73ef 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs @@ -39,7 +39,7 @@ namespace Barotrauma { var statusText = goal.StatusText(Traitor); var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}"; + return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; }).ToArray()), string.Join("", activeGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); @@ -49,7 +49,7 @@ namespace Barotrauma { var statusText = goal.StatusText(Traitor); var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}"; + return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; }).ToArray()), string.Join("", allGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); @@ -57,13 +57,14 @@ namespace Barotrauma public virtual IEnumerable StartMessageKeys => new string[] { "[traitorgoalinfos]" }; public virtual IEnumerable StartMessageValues => new string[] { GoalInfos }; - public virtual string StartMessageText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageTextId, StartMessageKeys, StartMessageValues); + public virtual LocalizedString StartMessageText + => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageTextId, StartMessageKeys.Zip(StartMessageValues, (k,v) => (k,v)).ToArray()); public virtual string StartMessageServerTextId { get; set; } = "TraitorObjectiveStartMessageServer"; public virtual IEnumerable StartMessageServerKeys => StartMessageKeys.Concat(new string[] { "[traitorname]" }); public virtual IEnumerable StartMessageServerValues => StartMessageValues.Concat(new string[] { Traitor?.Character?.Name ?? "(unknown)" }); - public virtual string StartMessageServerText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageServerTextId, StartMessageServerKeys, StartMessageServerValues); + public virtual LocalizedString StartMessageServerText => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageServerTextId, StartMessageServerKeys.Zip(StartMessageServerValues, (k,v) => (k,v)).ToArray()); public virtual string EndMessageSuccessTextId { get; set; } = "TraitorObjectiveEndMessageSuccess"; public virtual string EndMessageSuccessDeadTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDead"; @@ -83,7 +84,7 @@ namespace Barotrauma var messageId = IsCompleted ? (traitorIsDead ? EndMessageSuccessDeadTextId : traitorIsDetained ? EndMessageSuccessDetainedTextId : EndMessageSuccessTextId) : (traitorIsDead ? EndMessageFailureDeadTextId : traitorIsDetained ? EndMessageFailureDetainedTextId : EndMessageFailureTextId); - return TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, messageId, EndMessageKeys.ToArray(), EndMessageValues.ToArray()); + return TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, messageId, EndMessageKeys.Zip(EndMessageValues, (k,v)=>(k,v)).ToArray()); } } @@ -133,21 +134,21 @@ namespace Barotrauma IsStarted = true; - traitor.SendChatMessageBox(StartMessageText, traitor.Mission?.Identifier); - traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission?.Identifier); + traitor.SendChatMessageBox(StartMessageText.Value, traitor.Mission.Identifier); + traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission.Identifier); return true; } public void StartMessage() { - Traitor.SendChatMessage(StartMessageText, Traitor.Mission?.Identifier); + Traitor.SendChatMessage(StartMessageText.Value, Traitor.Mission.Identifier); } public void EndMessage() { - Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission?.Identifier); - Traitor.SendChatMessage(EndMessageText, Traitor.Mission?.Identifier); + Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission.Identifier); + Traitor.SendChatMessage(EndMessageText, Traitor.Mission.Identifier); } public void Update(float deltaTime) @@ -170,12 +171,12 @@ namespace Barotrauma pendingGoals.RemoveAt(i); if (GameMain.Server != null) { - Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission?.Identifier); + Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission.Identifier); if (pendingGoals.Count > 0) { - Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission?.Identifier); + Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission.Identifier); } - Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission?.Identifier); + Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission.Identifier); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index 7e42cff1b..ece1c941b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -22,37 +22,35 @@ namespace Barotrauma public delegate void MessageSender(string message); public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender) { - string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, new string[] { - "[codewords]", "[coderesponse]" - }, new string[] { - codeWords, codeResponse - }); + string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, + ("[codewords]", codeWords), + ("[coderesponse]", codeResponse)); messageSender(greetingMessage); Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character); Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection); if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null) { - GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText, Mission?.Identifier, TraitorMessageType.ServerMessageBox); + GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText.Value, Mission.Identifier, TraitorMessageType.ServerMessageBox); } } - public void SendChatMessage(string serverText, string iconIdentifier) + public void SendChatMessage(string serverText, Identifier iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.Server); } - public void SendChatMessageBox(string serverText, string iconIdentifier) + public void SendChatMessageBox(string serverText, Identifier iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.ServerMessageBox); } - public void UpdateCurrentObjective(string objectiveText, string iconIdentifier) + public void UpdateCurrentObjective(string objectiveText, Identifier iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); Character.TraitorCurrentObjective = objectiveText; - GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective, iconIdentifier, TraitorMessageType.Objective); + GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective.Value, iconIdentifier, TraitorMessageType.Objective); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index d35216f45..fe1c89f73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -169,7 +169,8 @@ namespace Barotrauma else { var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); - if (mission != null) { + if (mission != null) + { if (mission.CanBeStarted(server, this, CharacterTeamType.None)) { if (mission.Start(server, this, CharacterTeamType.None)) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index 1e16d660f..d2397ec46 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -43,7 +43,7 @@ namespace Barotrauma public string GlobalEndMessageFailureDeadTextId { get; private set; } public string GlobalEndMessageFailureDetainedTextId { get; private set; } - public readonly string Identifier; + public readonly Identifier Identifier; public virtual IEnumerable GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; public virtual IEnumerable GlobalEndMessageValues { @@ -60,7 +60,7 @@ namespace Barotrauma { get { - if (Traitors.Any() && allObjectives.Count > 0) + if (Traitors.Any() && allObjectives.Count > 0) { return TextManager.JoinServerMessages("\n", Traitors.Values.Select(traitor => @@ -71,7 +71,7 @@ namespace Barotrauma var messageId = isSuccess ? (traitorIsDead ? GlobalEndMessageSuccessDeadTextId : traitorIsDetained ? GlobalEndMessageSuccessDetainedTextId : GlobalEndMessageSuccessTextId) : (traitorIsDead ? GlobalEndMessageFailureDeadTextId : traitorIsDetained ? GlobalEndMessageFailureDetainedTextId : GlobalEndMessageFailureTextId); - return TextManager.FormatServerMessageWithGenderPronouns(traitor.Character?.Info?.Gender ?? Gender.None, messageId, GlobalEndMessageKeys.ToArray(), GlobalEndMessageValues.ToArray()); + return TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, messageId, GlobalEndMessageKeys.Zip(GlobalEndMessageValues).ToArray()); }).ToArray()); } return ""; @@ -376,7 +376,7 @@ namespace Barotrauma } } - public TraitorMission(string identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) + public TraitorMission(Identifier identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) { Identifier = identifier; StartText = startText; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs index cecc35b87..a9aa33810 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs @@ -9,38 +9,27 @@ namespace Barotrauma { class TraitorMissionPrefab { - public class TraitorMissionEntry + public class TraitorMissionEntry : Prefab { + public static PrefabCollection Prefabs => TraitorMissionPrefab.Prefabs; + public readonly TraitorMissionPrefab Prefab; public float SelectedWeight; - public TraitorMissionEntry(XElement element) + public TraitorMissionEntry(ContentXElement element, TraitorMissionsFile file) : base(file, element) { Prefab = new TraitorMissionPrefab(element); } - } - public static readonly List List = new List(); - public static void Init() - { - var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) - { - List.Add(new TraitorMissionEntry(element)); - } - } + public override void Dispose() { } } + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static TraitorMissionPrefab RandomPrefab() { - var selected = ToolBox.SelectWeightedRandom(List, List.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random); + var selected = ToolBox.SelectWeightedRandom(Prefabs.ToList(), Prefabs.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random); //the weight of the missions that didn't get selected keeps growing the make them more likely to get picked - foreach (var mission in List) + foreach (var mission in Prefabs) { mission.SelectedWeight += 10; } @@ -103,7 +92,7 @@ namespace Barotrauma private delegate bool TargetFilter(string value, Character character); private static Dictionary targetFilters = new Dictionary() { - { "job", (value, character) => value.Equals(character.Info.Job.Prefab.Identifier, StringComparison.OrdinalIgnoreCase) }, + { "job", (value, character) => value == character.Info.Job.Prefab.Identifier }, { "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) } }; @@ -180,12 +169,29 @@ namespace Barotrauma itemCountFilters.Add((character) => filter(attribute.Value, character)); } } - goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), Config.GetAttributeString("identifier", null), Config.GetAttributeBool("preferNew", true), Config.GetAttributeBool("allowNew", true), Config.GetAttributeBool("allowExisting", true), Config.GetAttributeFloat("percentage", -1f), Config.GetAttributeStringArray("allowedContainers", new string[] {"steelcabinet", "mediumsteelcabinet", "suppliescabinet"})); + goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), + Config.GetAttributeString("identifier", + null), + Config.GetAttributeBool("preferNew", + true), + Config.GetAttributeBool("allowNew", + true), + Config.GetAttributeBool("allowExisting", + true), + Config.GetAttributeFloat("percentage", + -1f), + Config.GetAttributeIdentifierArray("allowedContainers", + new string[] + { + "steelcabinet", + "mediumsteelcabinet", + "suppliescabinet" + }.ToIdentifiers())); break; case "replaceinventory": checker.Required("containers", "replacements"); checker.Optional("percentage"); - goal = new Traitor.GoalReplaceInventory(Config.GetAttributeStringArray("containers", new string[] { }), Config.GetAttributeStringArray("replacements", new string[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); + goal = new Traitor.GoalReplaceInventory(Config.GetAttributeIdentifierArray("containers", new Identifier[] { }), Config.GetAttributeIdentifierArray("replacements", new Identifier[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); break; case "reachdistancefromsub": checker.Optional("distance"); @@ -221,7 +227,7 @@ namespace Barotrauma break; case "keeptransformedalive": checker.Required("speciesname"); - goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeString("speciesname", null)); + goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeIdentifier("speciesname", Identifier.Empty)); break; default: GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error); @@ -425,7 +431,7 @@ namespace Barotrauma } public readonly Dictionary Roles = new Dictionary(); - public readonly string Identifier; + public readonly Identifier Identifier; public readonly string StartText; public readonly string EndMessageSuccessText; public readonly string EndMessageSuccessDeadText; @@ -575,14 +581,14 @@ namespace Barotrauma if (jobs != null) { var jobsSet = new HashSet(jobs.Select(job => job.ToLower(CultureInfo.InvariantCulture))); - filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower(CultureInfo.InvariantCulture))); + filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower().Value)); } return new Role(filters); } - public TraitorMissionPrefab(XElement missionRoot) + public TraitorMissionPrefab(ContentXElement missionRoot) { - Identifier = missionRoot.GetAttributeString("identifier", null); + Identifier = missionRoot.GetAttributeIdentifier("identifier", Identifier.Empty); foreach (var element in missionRoot.Elements()) { using (var checker = new AttributeChecker(element)) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ff47e4232..299d02f04 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 @@ -63,7 +64,6 @@ - diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml deleted file mode 100644 index 0dae570e8..000000000 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub new file mode 100644 index 000000000..cdd96a32b Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml new file mode 100644 index 000000000..a6d330d8d --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/info.txt b/Barotrauma/BarotraumaShared/LocalMods/info.txt similarity index 97% rename from Barotrauma/BarotraumaShared/Mods/info.txt rename to Barotrauma/BarotraumaShared/LocalMods/info.txt index d3ce905ba..290565fb3 100644 --- a/Barotrauma/BarotraumaShared/Mods/info.txt +++ b/Barotrauma/BarotraumaShared/LocalMods/info.txt @@ -1,4 +1,5 @@ ------------------------------------------------------------------------- +TODO: THIS IS VERY OUTDATED +------------------------------------------------------------------------ General ------------------------------------------------------------------------ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub deleted file mode 100644 index 38b34627a..000000000 Binary files a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png b/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png deleted file mode 100644 index c76c81542..000000000 Binary files a/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerRun.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerRun.xml deleted file mode 100644 index 6da1fe1c9..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerRun.xml +++ /dev/null @@ -1,23 +0,0 @@ - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml deleted file mode 100644 index db65d0370..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml +++ /dev/null @@ -1,20 +0,0 @@ - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml deleted file mode 100644 index ab9573b83..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml +++ /dev/null @@ -1,20 +0,0 @@ - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml deleted file mode 100644 index b46d3a4c0..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml +++ /dev/null @@ -1,23 +0,0 @@ - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml deleted file mode 100644 index 71035be33..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml deleted file mode 100644 index 8fcbeb57a..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png deleted file mode 100644 index 31f9101d9..000000000 Binary files a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/filelist.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/filelist.xml deleted file mode 100644 index 82367efc8..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/filelist.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index a5d3ca142..f13ea197e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -89,8 +89,8 @@ namespace Barotrauma set; } - public string SonarLabel; - public string SonarIconIdentifier; + public LocalizedString SonarLabel; + public Identifier SonarIconIdentifier; private bool inDetectable; @@ -172,13 +172,9 @@ namespace Barotrauma } SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f); string label = element.GetAttributeString("sonarlabel", ""); - SonarLabel = TextManager.Get(label, returnNull: true) ?? label; - SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); - string typeString = element.GetAttributeString("type", "Any"); - if (Enum.TryParse(typeString, out TargetType t)) - { - Type = t; - } + SonarLabel = TextManager.Get(label).Fallback(label); + SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", Identifier.Empty); + Type = element.GetAttributeEnum("type", TargetType.Any); Reset(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 4b2dc74dc..14c4333de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -239,7 +239,7 @@ namespace Barotrauma { throw new Exception($"Tried to create an enemy ai controller for human!"); } - if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase)) + if (Character.Params.Group == "human") { // Pet Character.TeamID = CharacterTeamType.FriendlyNPC; @@ -252,7 +252,7 @@ namespace Barotrauma List aiElements = new List(); List aiCommonness = new List(); - foreach (XElement element in mainElement.Elements()) + foreach (var element in mainElement.Elements()) { if (!element.Name.ToString().Equals("ai", StringComparison.OrdinalIgnoreCase)) { continue; } aiElements.Add(element); @@ -270,12 +270,12 @@ namespace Barotrauma //choose a random ai element MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); XElement aiElement = aiElements.Count == 1 ? aiElements[0] : ToolBox.SelectWeightedRandom(aiElements, aiCommonness, random); - foreach (XElement subElement in aiElement.Elements()) + foreach (var subElement in aiElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "chooserandom": - LoadSubElement(subElement.Elements().GetRandom(random)); + LoadSubElement(subElement.Elements().ToArray().GetRandom(random)); break; default: LoadSubElement(subElement); @@ -330,12 +330,13 @@ namespace Barotrauma return _aiParams; } } - private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false); + private CharacterParams.TargetParams GetTargetParams(string targetTag) => GetTargetParams(targetTag.ToIdentifier()); + private CharacterParams.TargetParams GetTargetParams(Identifier targetTag) => AIParams.GetTarget(targetTag, false); private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget)); - private string GetTargetingTag(AITarget aiTarget) + private Identifier GetTargetingTag(AITarget aiTarget) { - if (aiTarget?.Entity == null) { return null; } - string targetingTag = null; + if (aiTarget?.Entity == null) { return Identifier.Empty; } + string targetingTag = string.Empty; if (aiTarget.Entity is Character targetCharacter) { if (targetCharacter.IsDead) @@ -407,7 +408,7 @@ namespace Barotrauma { targetingTag = "room"; } - return targetingTag; + return targetingTag.ToIdentifier(); } public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -683,7 +684,7 @@ namespace Barotrauma //if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState //otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it //TODO: a way for pets to differentiate hostile and friendly humans? - if (attacker?.AiTarget != null && !targetCharacter.SpeciesName.Equals(GetTargetingTag(attacker.AiTarget), StringComparison.OrdinalIgnoreCase)) + if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget)) { // Attack the character that attacked the target we are protecting ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); @@ -999,7 +1000,7 @@ namespace Barotrauma hullWeights.Clear(); float hullMinSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 2); bool checkWaterLevel = !AIParams.PatrolFlooded || !AIParams.PatrolDry; - foreach (var hull in Hull.hullList) + foreach (var hull in Hull.HullList) { if (hull.Submarine == null) { continue; } if (hull.Submarine.TeamID != Character.Submarine.TeamID) { continue; } @@ -2004,7 +2005,7 @@ namespace Barotrauma bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine; - if (State == AIState.Attack && !IsAttackRunning && !IsCoolDownRunning) + if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning)) { // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; @@ -2324,7 +2325,7 @@ namespace Barotrauma if (item.Condition <= 0.0f) { if (!wasBroken) { PetBehavior?.OnEat(item); } - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } } } @@ -2438,7 +2439,7 @@ namespace Barotrauma if (targetCharacter == Character) { continue; } float valueModifier = 1; - string targetingTag = GetTargetingTag(aiTarget); + Identifier targetingTag = GetTargetingTag(aiTarget); if (targetCharacter != null) { // ignore if target is tagged to be explicitly ignored (Feign Death) @@ -2535,7 +2536,7 @@ namespace Barotrauma if (s.Submarine == null) { continue; } if (s.Submarine.Info.IsRuin) { continue; } bool isCharacterInside = Character.CurrentHull != null; - bool isInnerWall = s.prefab.Tags.Contains("inner"); + bool isInnerWall = s.Prefab.Tags.Contains("inner"); if (isInnerWall && !isCharacterInside) { // Ignore inner walls when outside (walltargets still work) @@ -3141,7 +3142,7 @@ namespace Barotrauma if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; } if (Character.Submarine == null) { - if (w.prefab.Tags.Contains("inner")) + if (w.Prefab.Tags.Contains("inner")) { if (!Character.AnimController.CanEnterSubmarine) { return false; } } @@ -3321,10 +3322,13 @@ namespace Barotrauma inactiveTriggers.Clear(); } + private bool TryResetOriginalState(string tag) => + TryResetOriginalState(tag.ToIdentifier()); + /// /// Resets the target's state to the original value defined in the xml. /// - private bool TryResetOriginalState(string tag) + private bool TryResetOriginalState(Identifier tag) { if (!modifiedParams.ContainsKey(tag)) { return false; } if (AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) @@ -3344,8 +3348,8 @@ namespace Barotrauma } } - private readonly Dictionary modifiedParams = new Dictionary(); - private readonly Dictionary tempParams = new Dictionary(); + private readonly Dictionary modifiedParams = new Dictionary(); + private readonly Dictionary tempParams = new Dictionary(); private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null) { @@ -3369,6 +3373,9 @@ namespace Barotrauma } private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) + => ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting); + + private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false) { if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 7438c42bf..79879e835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -833,10 +833,9 @@ namespace Barotrauma suitableContainer = null; if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i => { - if (i.IsThisOrAnyContainerIgnoredByAI(character)) { return 0; } + if (!i.HasAccess(character)) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } - if (!container.HasAccess(character)) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } var rootContainer = container.Item.GetRootContainer(); if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } @@ -888,21 +887,21 @@ namespace Barotrauma { if (!target.IsArrested && AddTargets(Character, target) && newOrder == null) { - var orderPrefab = Order.GetPrefab("reportintruders"); + var orderPrefab = OrderPrefab.Prefabs["reportintruders"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; if (target.IsEscorted) { if (!Character.IsPrisoner && target.IsPrisoner) { - string msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true); - Character.Speak(msg, ChatMessageType.Order); + LocalizedString msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", ("[roomname]", targetHull.DisplayName, FormatCapitals.No)); + Character.Speak(msg.Value, ChatMessageType.Order); speak = false; } else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable) { - string msg = TextManager.GetWithVariables("orderdialog.mentalcase", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true); - Character.Speak(msg, ChatMessageType.Order); + LocalizedString msg = TextManager.GetWithVariables("orderdialog.mentalcase", ("[roomname]", targetHull.DisplayName, FormatCapitals.No)); + Character.Speak(msg.Value, ChatMessageType.Order); speak = false; } } @@ -913,14 +912,14 @@ namespace Barotrauma { if (AddTargets(Character, hull) && newOrder == null) { - var orderPrefab = Order.GetPrefab("reportfire"); + var orderPrefab = OrderPrefab.Prefabs["reportfire"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } } if (IsBallastFloraNoticeable(Character, hull) && newOrder == null) { - var orderPrefab = Order.GetPrefab("reportballastflora"); + var orderPrefab = OrderPrefab.Prefabs["reportballastflora"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } @@ -932,7 +931,7 @@ namespace Barotrauma { if (AddTargets(Character, gap) && newOrder == null && !gap.IsRoomToRoom) { - var orderPrefab = Order.GetPrefab("reportbreach"); + var orderPrefab = OrderPrefab.Prefabs["reportbreach"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } @@ -947,7 +946,7 @@ namespace Barotrauma { if (AddTargets(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { - var orderPrefab = Order.GetPrefab("requestfirstaid"); + var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } @@ -961,7 +960,7 @@ namespace Barotrauma if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; } if (AddTargets(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { - var orderPrefab = Order.GetPrefab("reportbrokendevices"); + var orderPrefab = OrderPrefab.Prefabs["reportbrokendevices"]; newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character); targetHull = hull; } @@ -978,15 +977,18 @@ namespace Barotrauma { if (Character.TeamID == CharacterTeamType.FriendlyNPC) { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default, - identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"), + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, + identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order); + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order); #if SERVER - GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character)); + GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder + .WithManualPriority(CharacterInfo.HighestManualOrderPriority) + .WithTargetEntity(targetHull) + .WithOrderGiver(Character), "", null, Character)); #endif } } @@ -1025,17 +1027,17 @@ namespace Barotrauma if (Character.Oxygen < 20.0f) { - Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f); + Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); } if (Character.Bleeding > 2.0f) { - Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f); + Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); } if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) { - Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f); + Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); } } @@ -1191,21 +1193,21 @@ namespace Barotrauma case AIObjectiveCombat.CombatMode.Retreat: if (Character.IsSecurity) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f); } else { - Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.5f, "attackedbyfriendly", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("DialogAttackedByFriendly").Value, null, 0.5f, "attackedbyfriendly".ToIdentifier(), minDurationBetweenSimilar: 10.0f); } break; case AIObjectiveCombat.CombatMode.Offensive: case AIObjectiveCombat.CombatMode.Arrest: - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.5f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest").Value, null, 0.5f, "attackedbyfriendlysecurityarrest".ToIdentifier(), minDurationBetweenSimilar: 10.0f); break; case AIObjectiveCombat.CombatMode.None: if (Character.IsSecurity && realDamage > 1) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f); } break; } @@ -1415,14 +1417,14 @@ namespace Barotrauma } } - public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak = true) + public void SetOrder(Order order, bool speak = true) { - objectiveManager.SetOrder(order, option, priority, orderGiver, speak); + objectiveManager.SetOrder(order, speak); } - public void SetForcedOrder(Order order, string option, Character orderGiver) + public void SetForcedOrder(Order order) { - var objective = ObjectiveManager.CreateObjective(order, option, orderGiver); + var objective = ObjectiveManager.CreateObjective(order); ObjectiveManager.SetForcedOrder(objective); } @@ -1528,18 +1530,17 @@ namespace Barotrauma /// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second. /// To solve this, create a copy of the collection or change the code so that you first handle the first items and only after that query for the next items. /// - public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func predicate = null) + public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func predicate = null) { matchingItems.Clear(); items = matchingItems; - if (character == null) { return false; } - if (character.Inventory == null) { return false; } + if (character?.Inventory == null) { return false; } matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) && i.ConditionPercentage >= conditionPercentage && (!requireEquipped || character.HasEquippedItem(i)) && (predicate == null || predicate(i)), recursive, matchingItems); items = matchingItems; - return matchingItems.Any(i => i != null && (containedTag == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); + return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); } public static void StructureDamaged(Structure structure, float damageAmount, Character character) @@ -1594,7 +1595,7 @@ namespace Barotrauma (otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f); } } - otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning"), null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls", 10.0f); + otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f); someoneSpoke = true; } // React if we are security @@ -1674,7 +1675,7 @@ namespace Barotrauma GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); } item.StolenDuringRound = true; - otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); + otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); someoneSpoke = true; #if CLIENT HintManager.OnStoleItem(thief, item); @@ -1749,7 +1750,7 @@ namespace Barotrauma public static void RefreshTargets(Character character, Order order, Hull hull) { - switch (order.Identifier) + switch (order.Identifier.Value.ToLowerInvariant()) { case "reportfire": AddTargets(character, hull); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 2e651a325..83bcb0c42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -346,7 +346,7 @@ namespace Barotrauma } Ladder nextLadder = GetNextLadder(); var ladders = currentLadder ?? nextLadder; - bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && Math.Abs(steering.Y) > 1); + bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && steering.Y > 1); if (useLadders && character.SelectedConstruction != ladders.Item) { if (character.CanInteractWith(ladders.Item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs index 3f144bad8..3b74d869d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs @@ -128,7 +128,7 @@ namespace Barotrauma possibleTarget => HumanAIController.IsActive(possibleTarget) && (possibleTarget.TeamID != character.TeamID || mentalType == MentalType.Berserk) && humanAIController.VisibleHulls.Contains(possibleTarget.CurrentHull) && - possibleTarget != character).GetRandom(); + possibleTarget != character).GetRandomUnsynced(); if (mentalAttackTarget == null) { @@ -154,8 +154,8 @@ namespace Barotrauma // using this as an explicit time-out for the behavior. it's possible it will never run out because of the manager being disabled, but combat objective has failsafes for that mentalBehaviorTimer = MentalBehaviorInterval; humanAIController.AddCombatObjective(combatMode, mentalAttackTarget, allowHoldFire: holdFire, abortCondition: obj => mentalBehaviorTimer <= 0f); - string textIdentifier = $"dialogmentalstatereaction{combatMode.ToString().ToLowerInvariant()}"; - character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f); + Identifier textIdentifier = $"dialogmentalstatereaction{combatMode}".ToIdentifier(); + character.Speak(TextManager.Get(textIdentifier).Value, delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f); if (mentalType == MentalType.Berserk && !character.HasTeamChange(MentalTeamChange)) { @@ -169,8 +169,8 @@ namespace Barotrauma public void CreateDialogueBehavior(MentalType mentalType) { if (mentalType == MentalType.Normal) { return; } - string textIdentifier = $"dialogmentalstate{mentalType.ToString().ToLowerInvariant()}"; - character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f); + Identifier textIdentifier = $"dialogmentalstate{mentalType}".ToIdentifier(); + character.Speak(TextManager.Get(textIdentifier).Value, delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 39f35025e..53cec3b04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -3,182 +3,91 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma { + class NPCConversationCollection : Prefab + { + public static readonly Dictionary> Collections = new Dictionary>(); + + public readonly LanguageIdentifier Language; + + public readonly List Conversations; + public readonly Dictionary PersonalityTraits; + + public NPCConversationCollection(NPCConversationsFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + Language = element.GetAttributeIdentifier("language", "English").ToLanguageIdentifier(); + Conversations = new List(); + PersonalityTraits = new Dictionary(); + foreach (var subElement in element.Elements()) + { + Identifier elemName = new Identifier(subElement.Name.LocalName); + if (elemName == "Conversation") + { + Conversations.Add(new NPCConversation(subElement)); + } + else if (elemName == "PersonalityTrait") + { + var personalityTrait = new NPCPersonalityTrait(subElement); + PersonalityTraits.Add(personalityTrait.Name, personalityTrait); + } + } + } + + public override void Dispose() { } + } + class NPCConversation { const int MaxPreviousConversations = 20; - private class ConversationCollection - { - public readonly string Identifier; - - public readonly Dictionary> Conversations; - - public ConversationCollection(string identifier) - { - Identifier = identifier; - Conversations = new Dictionary>(); - } - - public void Add(string language, string filePath, XElement subElement) - { - if (!Conversations.ContainsKey(language)) - { - Conversations.Add(language, new List()); - } - Conversations[language].Add(new NPCConversation(subElement, filePath)); - } - - public void RemoveByFile(string filePath) - { - List keysToRemove = new List(); - foreach (var kpv in Conversations) - { - kpv.Value.RemoveAll(c => c.FilePath == filePath); - if (kpv.Value.Count == 0) { keysToRemove.Add(kpv.Key); } - } - - foreach (var key in keysToRemove) - { - Conversations.Remove(key); - } - } - } - - private static Dictionary allConversations = new Dictionary(); - - public readonly string FilePath; - public readonly string Line; - public readonly List AllowedJobs; + public readonly ImmutableHashSet AllowedJobs; - public readonly List Flags; + public readonly ImmutableHashSet Flags; //The line can only be selected when eventmanager intensity is between these values //null = no restriction - public float? maxIntensity, minIntensity; + public readonly float? maxIntensity, minIntensity; - public readonly List Responses; + public readonly ImmutableArray Responses; private readonly int speakerIndex; - private readonly List allowedSpeakerTags; + private readonly ImmutableHashSet allowedSpeakerTags; private readonly bool requireNextLine; // used primarily for team1 characters interacting with escorted personnel (TODO: not used anywhere) private readonly bool requireSight; - public static void LoadAll(IEnumerable files) + public NPCConversation(XElement element) { - foreach (var file in files) - { - if (Path.GetExtension(file.Path) == ".csv") continue; // .csv files are not supported - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - string language = doc.Root.GetAttributeString("Language", "English"); - string identifier = doc.Root.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"Conversations file '{file.Path}' has no identifier!"); - return; - } - - foreach (XElement subElement in doc.Root.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "conversation": - if (!allConversations.ContainsKey(identifier)) - { - allConversations.Add(identifier, new ConversationCollection(identifier)); - } - allConversations[identifier].Add(language, file.Path, subElement); - break; - case "personalitytrait": - new NPCPersonalityTrait(subElement, file.Path); - break; - } - } - } - - public static void RemoveByFile(string filePath) - { - List keysToRemove = new List(); - foreach (var kpv in allConversations) - { - kpv.Value.RemoveByFile(filePath); - if (!kpv.Value.Conversations.Any()) - { - keysToRemove.Add(kpv.Key); - } - } - - foreach (string key in keysToRemove) - { - allConversations.Remove(key); - } - - NPCPersonalityTrait.List.RemoveAll(npt => npt.FilePath == filePath); - } - - public NPCConversation(XElement element, string filePath) - { - FilePath = filePath; - Line = element.GetAttributeString("line", ""); speakerIndex = element.GetAttributeInt("speaker", 0); - AllowedJobs = new List(); - string allowedJobsStr = element.GetAttributeString("allowedjobs", ""); - foreach (string allowedJobIdentifier in allowedJobsStr.Split(',')) - { - string key = allowedJobIdentifier.ToLowerInvariant(); - if (JobPrefab.Prefabs.ContainsKey(key)) - { - AllowedJobs.Add(JobPrefab.Prefabs[key]); - } - } - - Flags = new List(element.GetAttributeStringArray("flags", new string[0])); - - allowedSpeakerTags = new List(); - string allowedSpeakerTagsStr = element.GetAttributeString("speakertags", ""); - foreach (string tag in allowedSpeakerTagsStr.Split(',')) - { - if (string.IsNullOrEmpty(tag)) continue; - allowedSpeakerTags.Add(tag.Trim().ToLowerInvariant()); - } + AllowedJobs = element.GetAttributeIdentifierArray("allowedjobs", Array.Empty()).ToImmutableHashSet(); + Flags = element.GetAttributeIdentifierArray("flags", Array.Empty()).ToImmutableHashSet(); + allowedSpeakerTags = element.GetAttributeIdentifierArray("speakertags", Array.Empty()).ToImmutableHashSet(); if (element.Attribute("minintensity") != null) minIntensity = element.GetAttributeFloat("minintensity", 0.0f); if (element.Attribute("maxintensity") != null) maxIntensity = element.GetAttributeFloat("maxintensity", 1.0f); - Responses = new List(); - foreach (XElement subElement in element.Elements()) - { - Responses.Add(new NPCConversation(subElement, filePath)); - } + Responses = element.Elements().Select(s => new NPCConversation(s)).ToImmutableArray(); requireNextLine = element.GetAttributeBool("requirenextline", false); requireSight = element.GetAttributeBool("requiresight", false); } - private static List GetCurrentFlags(Character speaker) + private static List GetCurrentFlags(Character speaker) { - var currentFlags = new List(); - if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep"); } + var currentFlags = new List(); + if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep".ToIdentifier()); } if (GameMain.GameSession != null && Level.Loaded != null) { if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial"); } + if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { @@ -187,30 +96,30 @@ namespace Barotrauma (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { - currentFlags.Add("EnterOutpost"); + currentFlags.Add("EnterOutpost".ToIdentifier()); } } if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f) { - currentFlags.Add("Casual"); + currentFlags.Add("Casual".ToIdentifier()); } if (GameMain.GameSession.IsCurrentLocationRadiated()) { - currentFlags.Add("InRadiation"); + currentFlags.Add("InRadiation".ToIdentifier()); } } if (speaker != null) { - if (speaker.AnimController.InWater) { currentFlags.Add("Underwater"); } - currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside"); + if (speaker.AnimController.InWater) { currentFlags.Add("Underwater".ToIdentifier()); } + currentFlags.Add((speaker.CurrentHull == null ? "Outside" : "Inside").ToIdentifier()); if (Character.Controlled != null) { if (Character.Controlled.CharacterHealth.GetAffliction("psychosis") != null) { - currentFlags.Add(speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf"); + currentFlags.Add((speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf").ToIdentifier()); } } @@ -218,7 +127,7 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag)) + if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag)) { currentFlags.Add(currentEffect.DialogFlag); } @@ -226,27 +135,27 @@ namespace Barotrauma if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost) { - currentFlags.Add("OutpostNPC"); + currentFlags.Add("OutpostNPC".ToIdentifier()); } if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None) { - currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType); + currentFlags.Add($"CampaignNPC.{speaker.CampaignInteractionType}".ToIdentifier()); } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && - (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false)) + (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned")) { if (speaker.TeamID == CharacterTeamType.None) { - currentFlags.Add("Bandit"); + currentFlags.Add("Bandit".ToIdentifier()); } else if (speaker.TeamID == CharacterTeamType.FriendlyNPC) { - currentFlags.Add("Hostage"); + currentFlags.Add("Hostage".ToIdentifier()); } } if (speaker.IsEscorted) { - currentFlags.Add("escort"); + currentFlags.Add("escort".ToIdentifier()); } } @@ -261,16 +170,16 @@ namespace Barotrauma List> lines = new List>(); CreateConversation(availableSpeakers, assignedSpeakers, null, lines, - availableConversations: allConversations.Values.SelectMany(cc => cc.Conversations.Where(kpv => kpv.Key == TextManager.Language).SelectMany(kpv => kpv.Value)).ToList()); + availableConversations: NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language].SelectMany(cc => cc.Conversations).ToList()); return lines; } - public static List> CreateRandom(List availableSpeakers, IEnumerable requiredFlags) + public static List> CreateRandom(List availableSpeakers, IEnumerable requiredFlags) { Dictionary assignedSpeakers = new Dictionary(); List> lines = new List>(); - var availableConversations = allConversations.Values.SelectMany(cc => cc.Conversations.SelectMany( - kpv => kpv.Value.Where(conversation => kpv.Key == TextManager.Language && requiredFlags.All(f => conversation.Flags.Contains(f))))).ToList(); + var availableConversations = NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language] + .SelectMany(cc => cc.Conversations.Where(c => requiredFlags.All(f => c.Flags.Contains(f)))).ToList(); if (availableConversations.Count > 0) { CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: false); @@ -282,11 +191,11 @@ namespace Barotrauma List availableSpeakers, Dictionary assignedSpeakers, NPCConversation baseConversation, - List> lineList, - List availableConversations, + IList> lineList, + IList availableConversations, bool ignoreFlags = false) { - List conversations = baseConversation == null ? availableConversations : baseConversation.Responses; + IList conversations = baseConversation == null ? availableConversations : baseConversation.Responses; if (conversations.Count == 0) { return; } int conversationIndex = Rand.Int(conversations.Count); @@ -390,7 +299,8 @@ namespace Barotrauma //check if the character has an appropriate job to say the line if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0) { - if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { return false; } + if (!(potentialSpeaker.Info?.Job?.Prefab is { } speakerJobPrefab) + || !selectedConversation.AllowedJobs.Contains(speakerJobPrefab.Identifier)) { return false; } } //check if the character has all required flags to say the line @@ -450,17 +360,13 @@ namespace Barotrauma { System.Text.StringBuilder sb = new System.Text.StringBuilder(); - foreach (string key in allConversations.Keys) + foreach (Identifier identifier in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language].Keys) { - foreach (string lang in allConversations[key].Conversations.Keys) + foreach (var current in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language][identifier].Conversations) { - if (lang != TextManager.Language) { continue; } - foreach (var current in allConversations[key].Conversations[lang]) - { - WriteConversation(sb, current, 0); - WriteSubConversations(sb, current.Responses, 1); - WriteEmptyRow(sb); - } + WriteConversation(sb, current, 0); + WriteSubConversations(sb, current.Responses, 1); + WriteEmptyRow(sb); } } @@ -480,15 +386,7 @@ namespace Barotrauma sb.Append(string.Join(",", conv.Flags)); // Flags sb.Append('*'); - for (int i = 0; i < conv.AllowedJobs.Count; i++) // Jobs - { - sb.Append(conv.AllowedJobs[i].Identifier); - - if (i < conv.AllowedJobs.Count - 1) - { - sb.Append(","); - } - } + sb.Append(string.Join(',', conv.AllowedJobs)); sb.Append('*'); sb.Append(string.Join(",", conv.allowedSpeakerTags)); // Traits @@ -501,13 +399,13 @@ namespace Barotrauma sb.AppendLine(); } - private static void WriteSubConversations(System.Text.StringBuilder sb, List responses, int depthIndex) + private static void WriteSubConversations(System.Text.StringBuilder sb, IList responses, int depthIndex) { for (int i = 0; i < responses.Count; i++) { WriteConversation(sb, responses[i], depthIndex); - if (responses[i].Responses != null && responses[i].Responses.Count > 0) + if (responses[i].Responses != null && responses[i].Responses.Length > 0) { WriteSubConversations(sb, responses[i].Responses, depthIndex + 1); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 2a76c67aa..5e618f9e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -10,8 +10,8 @@ namespace Barotrauma { public virtual float Devotion => AIObjectiveManager.baseDevotion; - public abstract string Identifier { get; set; } - public virtual string DebugTag => Identifier; + public abstract Identifier Identifier { get; set; } + public virtual string DebugTag => Identifier.Value; public virtual bool ForceRun => false; public virtual bool IgnoreUnsafeHulls => false; public virtual bool AbandonWhenCannotCompleteSubjectives => true; @@ -83,7 +83,7 @@ namespace Barotrauma public readonly Character character; public readonly AIObjectiveManager objectiveManager; - public string Option { get; private set; } + public readonly Identifier Option; private bool _abandon; public bool Abandon @@ -157,11 +157,11 @@ namespace Barotrauma return subObjective == null ? this : subObjective.GetActiveObjective(); } - public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null) + public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default) { this.objectiveManager = objectiveManager; this.character = character; - Option = option ?? string.Empty; + Option = option; PriorityModifier = priorityModifier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index ae6eb6ead..b76ebfaa3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -9,11 +9,11 @@ namespace Barotrauma { class AIObjectiveChargeBatteries : AIObjectiveLoop { - public override string Identifier { get; set; } = "charge batteries"; + public override Identifier Identifier { get; set; } = "charge batteries".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; private IEnumerable batteryList; - public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier) + public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier) : base(character, objectiveManager, priorityModifier, option) { } protected override bool Filter(PowerContainer battery) @@ -55,7 +55,7 @@ namespace Barotrauma { if (character == null || character.Submarine == null) { - return new PowerContainer[0]; + return Array.Empty(); } batteryList = character.Submarine.GetItems(true).Select(i => i.GetComponent()).Where(b => b != null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 007aee4a5..dfc2efe6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveCleanupItem : AIObjective { - public override string Identifier { get; set; } = "cleanup item"; + public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; @@ -61,21 +61,6 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (item.IgnoreByAI(character)) - { - Abandon = true; - return; - } - if (item.ParentInventory != null) - { - if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder())) - { - // Target was picked up or moved by someone. - Abandon = true; - return; - } - } - // Only continue when the get item sub objectives have been completed. if (subObjectives.Any()) { return; } if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) { @@ -133,7 +118,24 @@ namespace Barotrauma } } - protected override bool CheckObjectiveSpecific() => IsCompleted; + protected override bool CheckObjectiveSpecific() + { + if (item.IgnoreByAI(character)) + { + Abandon = true; + return false; + } + if (item.ParentInventory != null) + { + if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder())) + { + // Target was picked up or moved by someone. + Abandon = true; + return false; + } + } + return IsCompleted; + } public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index b84bf63ba..79732e006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveCleanupItems : AIObjectiveLoop { - public override string Identifier { get; set; } = "cleanup items"; + public override Identifier Identifier { get; set; } = "cleanup items".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; protected override bool ForceOrderPriority => false; @@ -79,18 +79,16 @@ namespace Barotrauma public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => allowUnloading && - !container.IgnoreByAI(character) && - container.IsInteractable(character) && + container.HasAccess(character) && container.HasTag("allowcleanup") && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && - container.GetComponent() is ItemContainer itemContainer && itemContainer.HasAccess(character) && + container.GetComponent() != null && IsItemInsideValidSubmarine(container, character); public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { if (item == null) { return false; } - if (item.IgnoreByAI(character)) { return false; } - if (!item.IsInteractable(character)) { return false; } + if (!item.HasAccess(character)) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if (item.ParentInventory != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 3de6f2a28..4f89fe0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveCombat : AIObjective { - public override string Identifier { get; set; } = "combat"; + public override Identifier Identifier { get; set; } = "combat".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; @@ -250,17 +250,17 @@ namespace Barotrauma case CombatMode.Offensive: if (TargetEliminated && objectiveManager.IsCurrentOrder()) { - character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f); + character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f); } break; case CombatMode.Arrest: - if (HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true)) + if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true)) { IsCompleted = true; } else if (Enemy.IsKnockedDown && !objectiveManager.IsCurrentObjective() && - !HumanAIController.HasItem(character, "handlocker", out _, requireEquipped: false)) + !HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false)) { IsCompleted = true; } @@ -399,7 +399,7 @@ namespace Barotrauma RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref followTargetObjective); TryAddSubObjective(ref seekWeaponObjective, - constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false) + constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false) { AllowStealing = HumanAIController.IsMentallyUnstable, EvaluateCombatPriority = false, // Use a custom formula instead @@ -636,7 +636,7 @@ namespace Barotrauma // If there's an item container that takes a battery, // assume that it's required for the stun effect // as we can't check the status effect conditions here. - var mobileBatteryTag = "mobilebattery"; + var mobileBatteryTag = "mobilebattery".ToIdentifier(); var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container && container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); @@ -848,7 +848,7 @@ namespace Barotrauma if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown) { - if (HumanAIController.HasItem(character, "handlocker", out _)) + if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _)) { if (!arrestingRegistered) { @@ -861,10 +861,10 @@ namespace Barotrauma { if (character.TeamID == CharacterTeamType.FriendlyNPC) { - ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs"); + ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier()); if (prefab != null) { - Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true); + Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true); } } RemoveFollowTarget(); @@ -914,7 +914,7 @@ namespace Barotrauma } } } - if (HumanAIController.HasItem(character, "handlocker", out IEnumerable matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy)) + if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy)) { var handCuffs = matchingItems.First(); if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true)) @@ -928,7 +928,7 @@ namespace Barotrauma return; } } - character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f); + character.Speak(TextManager.Get("DialogTargetArrested").Value, null, 3.0f, "targetarrested".ToIdentifier(), 30.0f); } if (!objectiveManager.IsCurrentObjective()) { @@ -939,7 +939,7 @@ namespace Barotrauma /// /// Seeks for more ammunition. Creates a new subobjective. /// - private void SeekAmmunition(string[] ammunitionIdentifiers) + private void SeekAmmunition(Identifier[] ammunitionIdentifiers) { retreatTarget = null; RemoveSubObjective(ref retreatObjective); @@ -974,7 +974,7 @@ namespace Barotrauma HumanAIController.UnequipEmptyItems(Weapon); RelatedItem item = null; Item ammunition = null; - string[] ammunitionIdentifiers = null; + Identifier[] ammunitionIdentifiers = null; if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) @@ -1212,17 +1212,17 @@ namespace Barotrauma retreatTarget = null; } - private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30); - private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0f, 1f), minDuration: 20); + private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30); + private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); - private void Speak(string textIdentifier, float delay, float minDuration) + private void Speak(Identifier textIdentifier, float delay, float minDuration) { if (character.IsOnPlayerTeam && !character.IsInFriendlySub) { - string msg = TextManager.Get(textIdentifier, true); - if (msg != null) + LocalizedString msg = TextManager.Get(textIdentifier); + if (!msg.IsNullOrEmpty()) { - character.Speak(msg, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration); + character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 26d2e8757..dea08ca34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -7,18 +7,18 @@ namespace Barotrauma { class AIObjectiveContainItem: AIObjective { - public override string Identifier { get; set; } = "contain item"; + public override Identifier Identifier { get; set; } = "contain item".ToIdentifier(); public Func GetItemPriority; - public string[] ignoredContainerIdentifiers; + public Identifier[] ignoredContainerIdentifiers; public bool checkInventory = true; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs and in some cases also enemy NPCs, like pirates) private readonly bool spawnItemIfNotFound; //can either be a tag or an identifier - public readonly string[] itemIdentifiers; + public readonly Identifier[] itemIdentifiers; public readonly ItemContainer container; private readonly Item item; public Item ItemToContain { get; private set; } @@ -60,25 +60,21 @@ namespace Barotrauma this.item = item; } - public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) - : this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { } + public AIObjectiveContainItem(Character character, Identifier itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) + : this(character, new Identifier[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { } - public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) + public AIObjectiveContainItem(Character character, Identifier[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { this.itemIdentifiers = itemIdentifiers; this.spawnItemIfNotFound = spawnItemIfNotFound; - for (int i = 0; i < itemIdentifiers.Length; i++) - { - itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); - } this.container = container; } protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } - if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI(character))) + if (container?.Item == null || !container.Item.HasAccess(character)) { Abandon = true; return false; @@ -89,23 +85,28 @@ namespace Barotrauma } else { - int containedItemCount = 0; - foreach (Item it in container.Inventory.AllItems) - { - if (CheckItem(it)) - { - containedItemCount++; - } - } - return containedItemCount >= ItemCount; + return CountItems(); } } - private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(character); + private bool CountItems() + { + int containedItemCount = 0; + foreach (Item it in container.Inventory.AllItems) + { + if (CheckItem(it)) + { + containedItemCount++; + } + } + return containedItemCount >= ItemCount; + } + + private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && i.HasAccess(character); protected override void Act(float deltaTime) { - if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character)) + if (container?.Item == null) { Abandon = true; return; @@ -141,8 +142,8 @@ namespace Barotrauma container.Inventory.TryPutItem(item, null); } } - IsCompleted = true; } + IsCompleted = item != null || CountItems(); } else { @@ -159,7 +160,7 @@ namespace Barotrauma { TargetName = container.Item.Name, AbortCondition = obj => - container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || + container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) || (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index dbcfad9d2..047c1b636 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveDecontainItem : AIObjective { - public override string Identifier { get; set; } = "decontain item"; + public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier(); public Func GetItemPriority; @@ -127,7 +127,7 @@ namespace Barotrauma RemoveExistingPredicate = RemoveExistingPredicate, RemoveMax = RemoveExistingMax, GetItemPriority = GetItemPriority, - ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null + ignoredContainerIdentifiers = sourceContainer != null ? new Identifier[] { sourceContainer.Item.Prefab.Identifier } : null }, onCompleted: () => IsCompleted = true, onAbandon: () => Abandon = true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index 7c689df98..5a57adc31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -6,7 +6,7 @@ namespace Barotrauma class AIObjectiveEscapeHandcuffs : AIObjective { // Used for prisoner escorts to allow them to escape their binds - public override string Identifier { get; set; } = "escape handcuffs"; + public override Identifier Identifier { get; set; } = "escape handcuffs".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; public override bool AllowOutsideSubmarine => true; public override bool AllowInAnySub => true; @@ -88,7 +88,7 @@ namespace Barotrauma escapeProgress += Rand.Range(2, 5); if (escapeProgress > 15) { - Item handcuffs = character.Inventory.FindItemByTag("handlocker"); + Item handcuffs = character.Inventory.FindItemByTag("handlocker".ToIdentifier()); if (handcuffs != null) { handcuffs.Drop(character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 251c0362b..4f818f175 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveExtinguishFire : AIObjective { - public override string Identifier { get; set; } = "extinguish fire"; + public override Identifier Identifier { get; set; } = "extinguish fire".ToIdentifier(); public override bool ForceRun => true; public override bool ConcurrentObjectives => true; public override bool KeepDivingGearOn => true; @@ -77,16 +77,16 @@ namespace Barotrauma private float sinTime; protected override void Act(float deltaTime) { - var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher"); + var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher".ToIdentifier()); if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem)) { TryAddSubObjective(ref getExtinguisherObjective, () => { - if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false)) + if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher".ToIdentifier(), allowBroken: false)) { - character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); + character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, "findextinguisher".ToIdentifier(), 30.0f); } - var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true) + var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher".ToIdentifier(), objectiveManager, equip: true) { AllowStealing = true, // If the item is inside an unsafe hull, decrease the priority @@ -94,7 +94,7 @@ namespace Barotrauma }; if (objectiveManager.HasOrder()) { - getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f); + getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher").Value, null, 0.0f, "dialogcannotfindfireextinguisher".ToIdentifier(), 10.0f); }; return getItemObjective; }); @@ -139,7 +139,7 @@ namespace Barotrauma extinguisher.Use(deltaTime, character); if (!targetHull.FireSources.Contains(fs)) { - character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } } if (move) @@ -147,7 +147,7 @@ namespace Barotrauma //go to the first firesource if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f)) { - DialogueIdentifier = "dialogcannotreachfire", + DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), TargetName = fs.Hull.DisplayName }, onAbandon: () => Abandon = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index 97d9450c2..a11672bcd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveExtinguishFires : AIObjectiveLoop { - public override string Identifier { get; set; } = "extinguish fires"; + public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier(); public override bool ForceRun => true; public override bool AllowInAnySub => true; @@ -27,7 +27,7 @@ namespace Barotrauma /// public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 500, hull.FireSources.Sum(fs => fs.Size.X))); - protected override IEnumerable GetList() => Hull.hullList; + protected override IEnumerable GetList() => Hull.HullList; protected override AIObjective ObjectiveConstructor(Hull target) => new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index ce20e9de1..0704b91c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveFightIntruders : AIObjectiveLoop { - public override string Identifier { get; set; } = "fight intruders"; + public override Identifier Identifier { get; set; } = "fight intruders".ToIdentifier(); protected override float IgnoreListClearInterval => 30; public override bool IgnoreUnsafeHulls => true; @@ -45,9 +45,9 @@ namespace Barotrauma { //hold fire while the enemy is in the airlock (except if they've attacked us) if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; } - return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase)); + return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock"); }; - character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f); + character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f); } } return combatObjective; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index cd3abcc19..c2c9f8ee5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -7,13 +7,13 @@ namespace Barotrauma { class AIObjectiveFindDivingGear : AIObjective { - public override string Identifier { get; set; } = "find diving gear"; + public override Identifier Identifier { get; set; } = "find diving gear".ToIdentifier(); public override string DebugTag => $"{Identifier} ({gearTag})"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AbandonWhenCannotCompleteSubjectives => false; - private readonly string gearTag; + private readonly Identifier gearTag; private AIObjectiveGetItem getDivingGear; private AIObjectiveContainItem getOxygen; @@ -21,13 +21,13 @@ namespace Barotrauma public const float MIN_OXYGEN = 10; - public const string HEAVY_DIVING_GEAR = "deepdiving"; - public const string LIGHT_DIVING_GEAR = "lightdiving"; + public static readonly Identifier HEAVY_DIVING_GEAR = "deepdiving".ToIdentifier(); + public static readonly Identifier LIGHT_DIVING_GEAR = "lightdiving".ToIdentifier(); /// /// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear) /// - public const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors"; - public const string OXYGEN_SOURCE = "oxygensource"; + public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier(); + public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier(); protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head); @@ -54,7 +54,7 @@ namespace Barotrauma { if (targetItem == null && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); + character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f); } return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { @@ -92,15 +92,15 @@ namespace Barotrauma { if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) { - character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f); + character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f); if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1) { - character.Speak(TextManager.Get("dialoglastoxygentank"), null, 0.0f, "dialoglastoxygentank", 30.0f); + character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f); } } else { - character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); + character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f); } } return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) @@ -130,7 +130,7 @@ namespace Barotrauma Abandon = true; if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f)) { - character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f); + character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f); } }, onCompleted: () => RemoveSubObjective(ref getOxygen)); @@ -147,11 +147,11 @@ namespace Barotrauma int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1); if (remainingOxygenTanks == 0) { - character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f); + character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f); } else if (remainingOxygenTanks < 10) { - character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f); + character.Speak(TextManager.Get("DialogLowOnOxygenTanks").Value, null, 0.0f, "lowonoxygentanks".ToIdentifier(), 30.0f); } return remainingOxygenTanks; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index dc2a12da2..f3380c63a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveFindSafety : AIObjective { - public override string Identifier { get; set; } = "find safety"; + public override Identifier Identifier { get; set; } = "find safety".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; @@ -317,7 +317,7 @@ namespace Barotrauma Hull bestHull = null; float bestValue = 0; bool bestIsAirlock = false; - foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h))) + foreach (Hull hull in Hull.HullList.OrderByDescending(h => EstimateHullSuitability(h))) { if (hull.Submarine == null) { continue; } // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it. @@ -342,7 +342,7 @@ namespace Barotrauma //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) if (hullSafety < bestValue) { continue; } //avoid airlock modules if not allowed to change the sub - if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t.Equals("airlock", StringComparison.OrdinalIgnoreCase))) + if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t == "airlock")) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 256913c4e..9e9a1be4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveFixLeak : AIObjective { - public override string Identifier { get; set; } = "fix leak"; + public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AllowInAnySub => true; @@ -64,15 +64,15 @@ namespace Barotrauma protected override void Act(float deltaTime) { - var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true); + var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true); if (weldingTool == null) { - TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), + TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onAbandon: () => { if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder()) { - character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f); + character.Speak(TextManager.Get("dialogcannotfindweldingequipment").Value, null, 0.0f, "dialogcannotfindweldingequipment".ToIdentifier(), 10.0f); } Abandon = true; }, @@ -91,7 +91,7 @@ namespace Barotrauma } if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel".ToIdentifier(), weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { RemoveExisting = true }, @@ -112,11 +112,11 @@ namespace Barotrauma int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1); if (remainingOxygenTanks == 0) { - character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f); + character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f); } else if (remainingOxygenTanks < 4) { - character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f); + character.Speak(TextManager.Get("DialogLowOnWeldingFuel").Value, null, 0.0f, "lowonweldingfuel".ToIdentifier(), 30.0f); } } return; @@ -142,7 +142,7 @@ namespace Barotrauma bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { - TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak), + TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak), onAbandon: () => Abandon = true, onCompleted: () => { @@ -160,7 +160,7 @@ namespace Barotrauma { UseDistanceRelativeToAimSourcePos = true, CloseEnough = reach, - DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null, + DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty, TargetName = Leak.FlowTargetHull?.DisplayName, CheckVisibility = false, requiredCondition = () => Leak.Submarine == character.Submarine, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 5f7d91295..9094affbe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveFixLeaks : AIObjectiveLoop { - public override string Identifier { get; set; } = "fix leaks"; + public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AllowInAnySub => true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index a0d14f7dc..4b5aba5dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveGetItem : AIObjective { - public override string Identifier { get; set; } = "get item"; + public override Identifier Identifier { get; set; } = "get item".ToIdentifier(); public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowMultipleInstances => true; @@ -21,7 +21,7 @@ namespace Barotrauma public float TargetCondition { get; set; } = 1; public bool AllowDangerousPressure { get; set; } - public readonly ImmutableArray IdentifiersOrTags; + public readonly ImmutableArray IdentifiersOrTags; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs) private bool spawnItemIfNotFound = false; @@ -32,8 +32,8 @@ namespace Barotrauma private bool isDoneSeeking; public Item TargetItem => targetItem; private int currSearchIndex; - public string[] ignoredContainerIdentifiers; - public string[] ignoredIdentifiersOrTags; + public Identifier[] ignoredContainerIdentifiers; + public Identifier[] ignoredIdentifiersOrTags; private AIObjectiveGoTo goToObjective; private float currItemPriority; private readonly bool checkInventory; @@ -83,10 +83,10 @@ namespace Barotrauma moveToTarget = targetItem?.GetRootInventoryOwner(); } - public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) - : this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { } + public AIObjectiveGetItem(Character character, Identifier identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) + : this(character, new Identifier[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { } - public AIObjectiveGetItem(Character character, IEnumerable identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) + public AIObjectiveGetItem(Character character, IEnumerable identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; @@ -97,27 +97,27 @@ namespace Barotrauma ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray(); } - public static IEnumerable ParseGearTags(IEnumerable identifiersOrTags) + public static IEnumerable ParseGearTags(IEnumerable identifiersOrTags) { - var tags = new List(); - foreach (string tag in identifiersOrTags) + var tags = new List(); + foreach (Identifier tag in identifiersOrTags) { - if (!tag.Contains('!')) + if (!tag.Contains("!")) { - tags.Add(tag.ToLowerInvariant()); + tags.Add(tag); } } return tags; } - public static IEnumerable ParseIgnoredTags(IEnumerable identifiersOrTags) + public static IEnumerable ParseIgnoredTags(IEnumerable identifiersOrTags) { - var ignoredTags = new List(); - foreach (string tag in identifiersOrTags) + var ignoredTags = new List(); + foreach (Identifier tag in identifiersOrTags) { - if (tag.Contains('!')) + if (tag.Contains("!")) { - ignoredTags.Add(tag.Remove("!").ToLowerInvariant()); + ignoredTags.Add(tag.Remove("!")); } } return ignoredTags; @@ -177,7 +177,7 @@ namespace Barotrauma if (dangerousPressure) { #if DEBUG - string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault(); + string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault().Value; DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow); #endif Abandon = true; @@ -480,7 +480,7 @@ namespace Barotrauma } else { - Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => + Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => { targetItem = spawnedItem; if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) @@ -528,14 +528,13 @@ namespace Barotrauma private bool CheckItem(Item item) { - if (!item.IsInteractable(character)) { return false; } - if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } + if (!item.HasAccess(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; - if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; } + if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; } - return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id)); + return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && item.Prefab.VariantOf == id)); } public override void Reset() @@ -575,9 +574,9 @@ namespace Barotrauma if (!character.IsOnPlayerTeam) { return; } if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; } if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; } - string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true); - if (msg == null) { return; } - character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); + LocalizedString msg = TextManager.Get(CannotFindDialogueIdentifierOverride, "dialogcannotfinditem"); + if (msg.IsNullOrEmpty() || !msg.Loaded) { return; } + character.Speak(msg.Value, identifier: "dialogcannotfinditem".ToIdentifier(), minDurationBetweenSimilar: 20.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index 9cb439547..0587b597f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveGetItems : AIObjective { - public override string Identifier { get; set; } = "get items"; + public override Identifier Identifier { get; set; } = "get items".ToIdentifier(); public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; public override bool AllowMultipleInstances => true; @@ -24,13 +24,13 @@ namespace Barotrauma public bool RequireLoaded { get; set; } public bool RequireAllItems { get; set; } - private readonly ImmutableArray gearTags; - private readonly string[] ignoredTags; + private readonly ImmutableArray gearTags; + private readonly Identifier[] ignoredTags; private bool subObjectivesCreated; public readonly HashSet achievedItems = new HashSet(); - public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) + public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray(); ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray(); @@ -47,7 +47,7 @@ namespace Barotrauma } if (!subObjectivesCreated) { - foreach (string tag in gearTags) + foreach (Identifier tag in gearTags) { if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; } int count = gearTags.Count(t => t == tag); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index dd250f619..2a1062948 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Barotrauma.Extensions; @@ -8,7 +9,7 @@ namespace Barotrauma { class AIObjectiveGoTo : AIObjective { - public override string Identifier { get; set; } = "go to"; + public override Identifier Identifier { get; set; } = "go to".ToIdentifier(); private AIObjectiveFindDivingGear findDivingGear; private readonly bool repeat; @@ -96,8 +97,8 @@ namespace Barotrauma public override bool AllowOutsideSubmarine => AllowGoingOutside; public override bool AllowInAnySub => true; - public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget"; - public string TargetName { get; set; } + public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier(); + public LocalizedString TargetName { get; set; } public ISpatialEntity Target { get; private set; } @@ -180,9 +181,11 @@ namespace Barotrauma if (DialogueIdentifier == null) { return; } if (!SpeakIfFails) { return; } if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; } - string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); - if (msg == null) { return; } - character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); + LocalizedString msg = TargetName == null ? + TextManager.Get(DialogueIdentifier) : + TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes); + if (msg.IsNullOrEmpty() || !msg.Loaded) { return; } + character.Speak(msg.Value, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); } public void ForceAct(float deltaTime) => Act(deltaTime); @@ -382,13 +385,23 @@ namespace Barotrauma { useScooter = false; checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); - string scooterTag = "scooter"; - string batteryTag = "mobilebattery"; + Identifier scooterTag = "scooter".ToIdentifier(); + Identifier batteryTag = "mobilebattery".ToIdentifier(); Item scooter = null; - float closeEnough = 250; - float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition); - bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!Mimic || - (targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); + bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false); + if (!shouldUseScooter) + { + float threshold = 500; + if (isInside) + { + Vector2 diff = Target.WorldPosition - character.WorldPosition; + shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150; + } + else + { + shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold; + } + } if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) { // Currently equipped scooter @@ -424,8 +437,7 @@ namespace Barotrauma } } } - bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter); - if (scooter != null && isScooterEquipped) + if (scooter != null && character.HasEquippedItem(scooter)) { if (shouldUseScooter) { @@ -534,6 +546,7 @@ namespace Barotrauma void UseScooter(Vector2 targetWorldPos) { + if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; } SteeringManager.Reset(); character.CursorPosition = targetWorldPos; if (character.Submarine != null) @@ -542,19 +555,26 @@ namespace Barotrauma } Vector2 diff = character.CursorPosition - character.Position; Vector2 dir = Vector2.Normalize(diff); - float sqrDist = diff.LengthSquared(); - if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f)) + if (character.CurrentHull == null && IsFollowOrderObjective) { - SteeringManager.SteeringManual(1.0f, dir); - } - else - { - float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2)); - bool isFacing = dot > 0.9f; - if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough)) + float sqrDist = diff.LengthSquared(); + if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f)) { SteeringManager.SteeringManual(1.0f, dir); } + else + { + float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2)); + bool isFacing = dot > 0.9f; + if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough)) + { + SteeringManager.SteeringManual(1.0f, dir); + } + } + } + else + { + SteeringManager.SteeringManual(1.0f, dir); } character.SetInput(InputType.Aim, false, true); character.SetInput(InputType.Shoot, false, true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 4be1b47fd..762e8cd08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveIdle : AIObjective { - public override string Identifier { get; set; } = "idle"; + public override Identifier Identifier { get; set; } = "idle".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; public override bool AllowInAnySub => true; @@ -93,7 +93,7 @@ namespace Barotrauma public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } - public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); + public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); public void CalculatePriority(float max = 0) { @@ -391,7 +391,7 @@ namespace Barotrauma { targetHulls.Clear(); hullWeights.Clear(); - foreach (var hull in Hull.hullList) + foreach (var hull in Hull.HullList) { if (character.Submarine == null) { break; } if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 9ec761e4c..cfd3e7ca9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveLoadItem : AIObjective { - public override string Identifier { get; set; } = "load item"; + public override Identifier Identifier { get; set; } = "load item".ToIdentifier(); public override bool IsLoop { get => true; @@ -20,9 +20,9 @@ namespace Barotrauma private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; } private Item Container { get; } private ItemContainer ItemContainer { get; } - private ImmutableArray TargetContainerTags { get; } - private ImmutableHashSet ValidContainableItemIdentifiers { get; } - private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); + private ImmutableArray TargetContainerTags { get; } + private ImmutableHashSet ValidContainableItemIdentifiers { get; } + private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); private int itemIndex = 0; private AIObjectiveDecontainItem decontainObjective; @@ -30,7 +30,7 @@ namespace Barotrauma private Item targetItem; private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable"; - public AIObjectiveLoadItem(Item container, ImmutableArray targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, string option, Character character, AIObjectiveManager objectiveManager, float priorityModifier) + public AIObjectiveLoadItem(Item container, ImmutableArray targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, Identifier option, Character character, AIObjectiveManager objectiveManager, float priorityModifier) : base(character, objectiveManager, priorityModifier) { Container = container; @@ -42,7 +42,7 @@ namespace Barotrauma } TargetContainerTags = targetTags; TargetItemCondition = targetCondition; - if (!string.IsNullOrEmpty(option)) + if (!option.IsEmpty) { string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}"; if (TextManager.ContainsTag(optionSpecificDialogueIdentifier)) @@ -63,7 +63,7 @@ namespace Barotrauma private enum CheckStatus { Unfinished, Finished } - private ImmutableHashSet GetValidContainableItemIdentifiers() + private ImmutableHashSet GetValidContainableItemIdentifiers() { if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers)) { @@ -75,7 +75,7 @@ namespace Barotrauma var potentialContainablePrefabs = MapEntityPrefab.List .Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i))) .Cast(); - var validContainableItemIdentifiers = new HashSet(); + var validContainableItemIdentifiers = new HashSet(); foreach (var component in Container.Components) { if (CheckComponent() == CheckStatus.Finished) @@ -125,7 +125,7 @@ namespace Barotrauma useDefaultContainableItemIdentifiers = false; if (statusEffect.TargetIdentifiers != null) { - foreach (string target in statusEffect.TargetIdentifiers) + foreach (Identifier target in statusEffect.TargetIdentifiers) { foreach (var prefab in potentialContainablePrefabs) { @@ -308,11 +308,9 @@ namespace Barotrauma if (rootInventoryOwner is Item parentItem) { if (parentItem.HasTag("donttakeitems")) { return false; } - if (!(parentItem.GetComponent()?.HasAccess(character) ?? true)) { return false; } } - if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } + if (!item.HasAccess(character)) { return false; } if (!character.HasItem(item) && !CanEquip(item)) { return false; } - if (!ItemContainer.HasAccess(character)) { return false; } if (!ItemContainer.CanBeContained(item)) { return false; } if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; } if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index 5d6eb0753..67a417900 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -9,11 +9,11 @@ namespace Barotrauma { class AIObjectiveLoadItems : AIObjectiveLoop { - public override string Identifier { get; set; } = "load items"; + public override Identifier Identifier { get; set; } = "load items".ToIdentifier(); protected override float IgnoreListClearInterval => 20.0f; protected override bool ResetWhenClearingIgnoreList => false; - private ImmutableArray TargetContainerTags { get; } + private ImmutableArray TargetContainerTags { get; } private List TargetContainers { get; } = new List(); private ItemCondition TargetCondition { get; } @@ -23,7 +23,7 @@ namespace Barotrauma Full } - public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, string option, ImmutableArray containerTags, Item targetContainer = null, float priorityModifier = 1) + public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, Identifier option, ImmutableArray containerTags, Item targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { if ((containerTags == null || containerTags.None()) && targetContainer == null) @@ -50,19 +50,17 @@ namespace Barotrauma return true; } - public static bool IsValidTarget(Item item, Character character, ImmutableArray? targetContainerTags = null, ItemCondition? targetCondition = null) + public static bool IsValidTarget(Item item, Character character, ImmutableArray? targetContainerTags = null, ItemCondition? targetCondition = null) { if (item == null) { return false; } if (item.Removed) { return false; } - if (targetContainerTags.HasValue && !Order.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; } + if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; } if (!(item.GetComponent() is ItemContainer container)) { return false; } if (container.Inventory == null) { return false; } if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; } - if (!item.IsInteractable(character)) { return false; } - if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } - if (!container.HasAccess(character)) { return false; } + if (!item.HasAccess(character)) { return false; } // Ignore items that require power but don't have it if (item.GetComponent() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 55b32ce12..38aa4b5d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -36,7 +36,7 @@ namespace Barotrauma return false; } - public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null) + public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default) : base(character, objectiveManager, priorityModifier, option) { } protected override void Act(float deltaTime) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 44cc68d5a..e77eceb92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -38,7 +38,7 @@ namespace Barotrauma } } - public List CurrentOrders { get; } = new List(); + public List CurrentOrders { get; } = new List(); /// /// The AIObjective in with the highest /// @@ -123,23 +123,23 @@ namespace Barotrauma int objectiveCount = Objectives.Count; foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { - var orderPrefab = Order.GetPrefab(autonomousObjective.identifier); - if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); } + var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier]; + if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); } Item item = null; if (orderPrefab.MustSetTarget) { - item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character, orderOption: autonomousObjective.option)?.GetRandom(); + item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced(); } - var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); + var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if ((order.IgnoreAtOutpost || autonomousObjective.ignoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { continue; } } - var objective = CreateObjective(order, autonomousObjective.option, character, autonomousObjective.priorityModifier); + var objective = CreateObjective(order, autonomousObjective.PriorityModifier); if (objective != null && objective.CanBeCompleted) { AddObjective(objective, delay: Rand.Value() / 2); @@ -324,7 +324,7 @@ namespace Barotrauma SortObjectives(); } - public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak) + public void SetOrder(Order order, bool speak) { if (character.IsDead) { @@ -336,13 +336,13 @@ namespace Barotrauma } ClearIgnored(); - if (order == null || order.Identifier == "dismissed") + if (order == null || order.IsDismissal) { - if (!string.IsNullOrEmpty(option)) + if (order.Option != Identifier.Empty) { - if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option))) + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(order.Option))) { - var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option)); + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(order.Option)); CurrentOrders.Remove(dismissedOrderInfo); } } @@ -357,18 +357,18 @@ namespace Barotrauma { if (CurrentOrders.Count <= i) { break; } var currentOrder = CurrentOrders[i]; - if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option)) + if (currentOrder.Objective == null || currentOrder.MatchesOrder(order)) { CurrentOrders.RemoveAt(i); continue; } - var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption); - if (currentOrderInfo.HasValue) + var currentOrderInfo = character.GetCurrentOrder(currentOrder); + if (currentOrderInfo is Order) { - int currentPriority = currentOrderInfo.Value.ManualPriority; + int currentPriority = currentOrderInfo.ManualPriority; if (currentOrder.ManualPriority != currentPriority) { - CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority); + CurrentOrders[i] = currentOrder.WithManualPriority(currentPriority); } } else @@ -377,46 +377,46 @@ namespace Barotrauma } } - var newCurrentOrder = CreateObjective(order, option, orderGiver); - if (newCurrentOrder != null) + var newCurrentObjective = CreateObjective(order); + if (newCurrentObjective != null) { - newCurrentOrder.Abandoned += () => DismissSelf(order, option); - CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder)); + newCurrentObjective.Abandoned += () => DismissSelf(order); + CurrentOrders.Add(order.WithObjective(newCurrentObjective)); } if (!HasOrders()) { // Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding) CreateAutonomousObjectives(); } - else if (newCurrentOrder != null) + else if (newCurrentObjective != null) { if (speak && character.IsOnPlayerTeam) { - string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative"); - character.Speak(msg, delay: 1.0f); + LocalizedString msg = newCurrentObjective.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative"); + character.Speak(msg.Value, delay: 1.0f); } } } - public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1) + public AIObjective CreateObjective(Order order, float priorityModifier = 1) { - if (order == null || order.Identifier == "dismissed") { return null; } + if (order == null || order.IsDismissal) { return null; } AIObjective newObjective; - switch (order.Identifier.ToLowerInvariant()) + switch (order.Identifier.Value.ToLowerInvariant()) { case "follow": - if (orderGiver == null) { return null; } - newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier) + if (order.OrderGiver == null) { return null; } + newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { CloseEnough = Rand.Range(80f, 100f), - CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), + CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), ExtraDistanceOutsideSub = 100, ExtraDistanceWhileSwimming = 100, AllowGoingOutside = true, IgnoreIfTargetDead = true, IsFollowOrderObjective = true, Mimic = character.IsOnPlayerTeam, - DialogueIdentifier = "dialogcannotreachplace" + DialogueIdentifier = "dialogcannotreachplace".ToIdentifier() }; break; case "wait": @@ -426,14 +426,14 @@ namespace Barotrauma }; break; case "return": - newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier); - newObjective.Completed += () => DismissSelf(order, option); + newObjective = new AIObjectiveReturn(character, order.OrderGiver, this, priorityModifier: priorityModifier); + newObjective.Completed += () => DismissSelf(order); break; case "fixleaks": newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull); break; case "chargebatteries": - newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier); + newObjective = new AIObjectiveChargeBatteries(character, this, order.Option, priorityModifier); break; case "rescue": newObjective = new AIObjectiveRescueAll(character, this, priorityModifier); @@ -450,16 +450,16 @@ namespace Barotrauma if (order.TargetItemComponent is Pump targetPump) { if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } - newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier) + newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier) { IsLoop = false, - Override = orderGiver != null && orderGiver.IsCommanding + Override = order.OrderGiver is { IsCommanding: true } }; - newObjective.Completed += () => DismissSelf(order, option); + newObjective.Completed += () => DismissSelf(order); } else { - newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier); + newObjective = new AIObjectivePumpWater(character, this, order.Option, priorityModifier: priorityModifier); } break; case "extinguishfires": @@ -479,22 +479,22 @@ namespace Barotrauma if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; } if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && orderGiver.IsCommanding + Override = order.OrderGiver != null && order.OrderGiver.IsCommanding }; break; case "setchargepct": - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier) + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier) { IsLoop = false, Override = !character.IsDismissed, completionCondition = () => { - if (float.TryParse(option, out float pct)) + if (float.TryParse(order.Option.Value, out float pct)) { var targetRatio = Math.Clamp(pct, 0f, 1f); var currentRatio = (order.TargetItemComponent as PowerContainer).RechargeRatio; @@ -532,7 +532,7 @@ namespace Barotrauma newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier); break; case "prepareforexpedition": - newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems) + newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems) { KeepActiveWhenReady = true, CheckInventory = true, @@ -548,7 +548,7 @@ namespace Barotrauma } else { - prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems) + prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems) { KeepActiveWhenReady = false, CheckInventory = false, @@ -559,20 +559,20 @@ namespace Barotrauma prepareObjective.KeepActiveWhenReady = false; prepareObjective.Equip = true; newObjective = prepareObjective; - newObjective.Completed += () => DismissSelf(order, option); + newObjective.Completed += () => DismissSelf(order); break; case "loaditems": - newObjective = new AIObjectiveLoadItems(character, this, option, order.GetTargetItems(option), order.TargetEntity as Item, priorityModifier); + newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier); break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && orderGiver.IsCommanding + Override = order.OrderGiver != null && order.OrderGiver.IsCommanding }; if (newObjective.Abandon) { return null; } break; @@ -585,27 +585,26 @@ namespace Barotrauma return newObjective; } - private void DismissSelf(Order order, string option) + private void DismissSelf(Order order) { - var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order, option)); - if (currentOrder.Order == null) + var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option)); + if (currentOrder == null) { #if DEBUG DebugConsole.ThrowError("Tried to self-dismiss an order, but no matching current order was found"); #endif return; } - Order dismissOrder = Order.GetPrefab("dismissed"); - var orderOption = Order.GetDismissOrderOption(currentOrder); - int priority = currentOrder.ManualPriority; + + Order dismissOrder = currentOrder.GetDismissal(); #if CLIENT if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) { - GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder, orderOption, priority, character); + GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder); } #else - GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, orderOption, priority, currentOrder.Order.TargetSpatialEntity, character, character)); - SetOrder(dismissOrder, orderOption, priority, character, speak: false); + GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character)); + SetOrder(dismissOrder, speak: false); #endif } @@ -695,7 +694,7 @@ namespace Barotrauma return 0; } - public OrderInfo? GetCurrentOrderInfo() + public Order GetCurrentOrderInfo() { if (currentOrder == null) { return null; } return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 5c7055788..80e9f3371 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveOperateItem : AIObjective { - public override string Identifier { get; set; } = "operate item"; + public override Identifier Identifier { get; set; } = "operate item".ToIdentifier(); public override string DebugTag => $"{Identifier} {component.Name}"; public override bool AllowAutomaticItemUnequipping => true; @@ -79,7 +79,7 @@ namespace Barotrauma return Priority; } } - switch (Option) + switch (Option.Value.ToLowerInvariant()) { case "shutdown": if (!reactor.PowerOn) @@ -146,7 +146,7 @@ namespace Barotrauma return Priority; } - public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, + public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, Identifier option, bool requireEquip, Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { @@ -181,7 +181,7 @@ namespace Barotrauma { if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f); + character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name).Value, delay: 2.0f, identifier: "cantfindcontroller".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } Abandon = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index 9bea9ed11..d83f75768 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectivePrepare : AIObjective { - public override string Identifier { get; set; } = "prepare"; + public override Identifier Identifier { get; set; } = "prepare".ToIdentifier(); public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; public override bool KeepDivingGearOnAlsoWhenInactive => true; @@ -19,8 +19,8 @@ namespace Barotrauma private AIObjectiveGetItems getMultipleItemsObjective; private bool subObjectivesCreated; private readonly Item targetItem; - private readonly ImmutableArray requiredItems; - private readonly ImmutableArray optionalItems; + private readonly ImmutableArray requiredItems; + private readonly ImmutableArray optionalItems; private readonly HashSet items = new HashSet(); public bool KeepActiveWhenReady { get; set; } public bool CheckInventory { get; set; } @@ -43,7 +43,7 @@ namespace Barotrauma this.targetItem = targetItem; } - public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable optionalItems, IEnumerable requiredItems = null, float priorityModifier = 1) + public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable optionalItems, IEnumerable requiredItems = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { this.optionalItems = optionalItems.ToImmutableArray(); @@ -98,7 +98,7 @@ namespace Barotrauma { getAllItemsObjective = CreateObjectives(requiredItems, requireAll: true); } - AIObjectiveGetItems CreateObjectives(IEnumerable itemTags, bool requireAll) + AIObjectiveGetItems CreateObjectives(IEnumerable itemTags, bool requireAll) { AIObjectiveGetItems objectiveReference = null; if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags) @@ -148,7 +148,7 @@ namespace Barotrauma } else { - IEnumerable allItems = optionalItems; + IEnumerable allItems = optionalItems; if (requiredItems != null && requiredItems.Any()) { allItems = requiredItems; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index d0c4045e8..f4537800a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -9,13 +9,13 @@ namespace Barotrauma { class AIObjectivePumpWater : AIObjectiveLoop { - public override string Identifier { get; set; } = "pump water"; + public override Identifier Identifier { get; set; } = "pump water".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; private IEnumerable pumpList; - public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1) + public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { } protected override void FindTargets() @@ -48,7 +48,7 @@ namespace Barotrauma { if (pumpList == null) { - if (character == null || character.Submarine == null) { return new Pump[0]; } + if (character == null || character.Submarine == null) { return Array.Empty(); } pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent()).Where(p => p != null); } return pumpList; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index b2115b8ba..adf82ad04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveRepairItem : AIObjective { - public override string Identifier { get; set; } = "repair item"; + public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); public override bool AllowInAnySub => true; @@ -70,7 +70,7 @@ namespace Barotrauma float reduction = isPriority ? 1 : isSelected ? 2 : 3; float max = AIObjectiveManager.LowestOrderPriority - reduction; float highestWeight = -1; - foreach (string tag in Item.Prefab.Tags) + foreach (Identifier tag in Item.Prefab.Tags) { if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight) { @@ -92,7 +92,7 @@ namespace Barotrauma IsCompleted = Item.IsFullCondition; if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) { - character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "itemrepaired".ToIdentifier(), 10.0f); } return IsCompleted; } @@ -118,7 +118,7 @@ namespace Barotrauma { if (character.IsOnPlayerTeam) { - getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f); + getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair").Value, null, 0.0f, "dialogcannotfindrequireditemtorepair".ToIdentifier(), 10.0f); } } subObjectives.Add(getItemObjective); @@ -206,7 +206,7 @@ namespace Barotrauma { if (character.IsOnPlayerTeam && IsRepairing()) { - character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f); } repairable.StopRepairing(character); } @@ -243,7 +243,7 @@ namespace Barotrauma Abandon = true; if (character.IsOnPlayerTeam && IsRepairing()) { - character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f); } }); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 92f1e67e6..a8b001bb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -9,12 +9,12 @@ namespace Barotrauma { class AIObjectiveRepairItems : AIObjectiveLoop { - public override string Identifier { get; set; } = "repair items"; + public override Identifier Identifier { get; set; } = "repair items".ToIdentifier(); /// /// If set, only fix items where required skill matches this. /// - public string RelevantSkill; + public Identifier RelevantSkill; public Item PrioritizedItem { get; private set; } @@ -72,9 +72,9 @@ namespace Barotrauma if (NearlyFullCondition(item)) { return false; } } } - if (!string.IsNullOrWhiteSpace(RelevantSkill)) + if (!RelevantSkill.IsEmpty) { - if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; } + if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; } } return !HumanAIController.IsItemRepairedByAnother(item, out _); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 3b0b1499f..7b460210f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveRescue : AIObjective { - public override string Identifier { get; set; } = "rescue"; + public override Identifier Identifier { get; set; } = "rescue".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; @@ -146,9 +146,10 @@ namespace Barotrauma { if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) { - character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", + ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundunconscioustarget{targetCharacter.Name}".ToIdentifier(), 60.0f); } // Go to the target and select it if (!character.CanInteractWith(targetCharacter)) @@ -158,7 +159,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, - DialogueIdentifier = "dialogcannotreachpatient", + DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), TargetName = targetCharacter.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), @@ -216,7 +217,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, - DialogueIdentifier = "dialogcannotreachpatient", + DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), TargetName = targetCharacter.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), @@ -233,18 +234,19 @@ namespace Barotrauma { if (targetCharacter.CurrentHull?.DisplayName != null) { - character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", + ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundwoundedtarget{targetCharacter.Name}".ToIdentifier(), 60.0f); } } GiveTreatment(deltaTime); } } - private readonly List suitableItemIdentifiers = new List(); - private readonly List itemNameList = new List(); - private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); + private readonly List suitableItemIdentifiers = new List(); + private readonly List itemNameList = new List(); + private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { if (targetCharacter == null) @@ -281,7 +283,7 @@ namespace Barotrauma if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } float bestSuitability = 0.0f; Item bestItem = null; - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) @@ -311,12 +313,12 @@ namespace Barotrauma { itemNameList.Clear(); suitableItemIdentifiers.Clear(); - foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) + foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) { if (treatmentSuitability.Value <= cprSuitability) { continue; } if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab) { - if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; } + if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; } suitableItemIdentifiers.Add(treatmentSuitability.Key); //only list the first 4 items if (itemNameList.Count < 4) @@ -327,7 +329,7 @@ namespace Barotrauma } if (itemNameList.Any()) { - string itemListStr = ""; + LocalizedString itemListStr = ""; if (itemNameList.Count == 1) { itemListStr = itemNameList[0]; @@ -337,33 +339,34 @@ namespace Barotrauma //[treatment1] or [treatment2] itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsLast", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemNameList[0], itemNameList[1] }); + ("[treatment1]", itemNameList[0]), + ("[treatment2]", itemNameList[1])); } else { //[treatment1], [treatment2], [treatment3] ... or [treatmentx] itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsFirst", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemNameList[0], itemNameList[1] }); + ("[treatment1]", itemNameList[0]), + ("[treatment2]", itemNameList[1])); for (int i = 2; i < itemNameList.Count - 1; i++) { itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsFirst", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemListStr, itemNameList[i] }); + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList[i])); } itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsLast", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemListStr, itemNameList.Last() }); + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList.Last())); } if (targetCharacter != character && character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" }, - new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }), - null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", + ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value, + null, 2.0f, $"listrequiredtreatments{targetCharacter.Name}".ToIdentifier(), 60.0f); } RemoveSubObjective(ref getItemObjective); TryAddSubObjective(ref getItemObjective, @@ -374,13 +377,13 @@ namespace Barotrauma Abandon = true; if (character != targetCharacter && character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); } }); } else if (cprSuitability <= 0) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; } } @@ -388,7 +391,7 @@ namespace Barotrauma else if (!targetCharacter.IsUnconscious) { //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; return; } @@ -425,7 +428,7 @@ namespace Barotrauma } if (remove) { - Entity.Spawner?.AddToRemoveQueue(item); + Entity.Spawner?.AddItemToRemoveQueue(item); } } @@ -434,8 +437,8 @@ namespace Barotrauma bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), - null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name).Value, + null, 1.0f, $"targethealed{targetCharacter.Name}".ToIdentifier(), 60.0f); } return isCompleted; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index ed1684d2c..e9cd9e4bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -7,7 +7,7 @@ namespace Barotrauma { class AIObjectiveRescueAll : AIObjectiveLoop { - public override string Identifier { get; set; } = "rescue all"; + public override Identifier Identifier { get; set; } = "rescue all".ToIdentifier(); public override bool ForceRun => true; public override bool InverseTargetEvaluation => true; public override bool AllowOutsideSubmarine => true; @@ -112,12 +112,15 @@ namespace Barotrauma { if (GetVitalityFactor(target) >= vitalityThreshold) { return false; } } - if (target.Submarine != character.Submarine) { return false; } if (character.Submarine != null) { // Don't allow going into another sub, unless it's connected and of the same team and type. if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; } } + else + { + return target.Submarine == null; + } if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { // Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index 31f2b4d75..c96c97b8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveReturn : AIObjective { - public override string Identifier { get; set; } = "return"; + public override Identifier Identifier { get; set; } = "return".ToIdentifier(); public Submarine ReturnTarget { get; } private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; @@ -93,7 +93,7 @@ namespace Barotrauma // Target the closest airlock float closestDist = 0; Hull airlock = null; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != targetHull.Submarine) { continue; } if (!hull.IsTaggedAirlock()) { continue; } @@ -210,10 +210,10 @@ namespace Barotrauma SteeringManager?.Reset(); if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) { - string msg = TextManager.Get("dialogcannotreturn", returnNull: true); - if (msg != null) + string msg = TextManager.Get("dialogcannotreturn").Value; + if (!msg.IsNullOrEmpty()) { - character.Speak(msg, identifier: "dialogcannotreturn", minDurationBetweenSimilar: 5.0f); + character.Speak(msg, identifier: "dialogcannotreturn".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 4bc797d54..9c1b018f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -18,102 +18,51 @@ namespace Barotrauma Operate } - struct OrderInfo + class OrderCategoryIcon : Prefab { - public readonly Order Order; - public readonly string OrderOption; - public readonly int ManualPriority; - public readonly OrderType Type; - public readonly AIObjective Objective; - public bool IsCurrentOrder => Type == OrderType.Current; + public readonly static PrefabCollection OrderCategoryIcons = new PrefabCollection(); - public enum OrderType + public OrderCategoryIcon(ContentXElement element, OrdersFile file) : base(file, element.GetAttributeIdentifier("category", "")) { - Current, - Previous + Category = Enum.Parse(Identifier.Value, true); + var spriteElement = element.GetChildElement("sprite"); + Sprite = new Sprite(spriteElement, lazyLoad: true); + Color = element.GetAttributeColor("color", Color.White); } - private OrderInfo(Order order, string orderOption, int manualPriority, OrderType orderType, AIObjective objective) - { - Order = order; - OrderOption = orderOption; - ManualPriority = Math.Min(manualPriority, CharacterInfo.HighestManualOrderPriority); - Type = orderType; - Objective = objective; - } + public readonly OrderCategory Category; + public readonly Sprite Sprite; + public readonly Color Color; - public OrderInfo(Order order, string orderOption) : this(order, orderOption, CharacterInfo.HighestManualOrderPriority, null) { } - - public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { } - - public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { } - - public OrderInfo(OrderInfo orderInfo, int manualPriority) : this(orderInfo.Order, orderInfo.OrderOption, manualPriority, orderInfo.Type, orderInfo.Objective) { } - - public OrderInfo(OrderInfo orderInfo, OrderType type) : this(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, type, orderInfo.Objective) { } - - public bool MatchesOrder(string orderIdentifier, string orderOption) => - (orderIdentifier == Order?.Identifier || (string.IsNullOrEmpty(orderIdentifier) && string.IsNullOrEmpty(Order?.Identifier))) && - (orderOption == OrderOption || (string.IsNullOrEmpty(orderOption) && string.IsNullOrEmpty(OrderOption))); - - public bool MatchesOrder(Order order, string option) => - MatchesOrder(order?.Identifier, option); - - public bool MatchesOrder(OrderInfo orderInfo) => - MatchesOrder(orderInfo.Order?.Identifier, orderInfo.OrderOption); - - public bool MatchesDismissedOrder(string dismissOrderOption) - { - string[] dismissedOrder = dismissOrderOption?.Split('.'); - if (dismissedOrder != null && dismissedOrder.Length > 0) - { - string dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : null; - if (dismissedOrderIdentifier == null || dismissedOrderIdentifier != Order?.Identifier) { return false; } - string dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : null; - if (dismissedOrderOption == null && string.IsNullOrEmpty(OrderOption)) { return true; } - return dismissedOrderOption == OrderOption; - } - else - { - return false; - } - } + public override void Dispose() { Sprite?.Remove(); } } - class Order + class OrderPrefab : PrefabWithUintIdentifier { - public static Dictionary Prefabs { get; private set; } - public static Dictionary> OrderCategoryIcons { get; private set; } - public static List PrefabList { get; private set; } - public static Order GetPrefab(string identifier) - { - if (!Prefabs.TryGetValue(identifier, out Order order)) - { - DebugConsole.ThrowError($"Cannot find an order with the identifier '{identifier}'!"); - } - return order; - } + public readonly static PrefabCollection Prefabs = new PrefabCollection(); - public Order Prefab { get; private set; } + public readonly static Identifier DismissalIdentifier = "dismissed".ToIdentifier(); + public static OrderPrefab Dismissal => Prefabs[DismissalIdentifier]; - public readonly string Name; + public readonly OrderCategory? Category; + public readonly Identifier CategoryIdentifier; + + public readonly LocalizedString Name; /// /// Name that can be used with the contextual version of the order /// - public readonly string ContextualName; + public readonly LocalizedString ContextualName; public readonly Sprite SymbolSprite; public readonly Type ItemComponentType; public readonly bool CanTypeBeSubclass; - public readonly ImmutableArray TargetItems; - public readonly ImmutableArray RequireItems; - private readonly Dictionary> OptionTargetItems; + public readonly ImmutableArray TargetItems; + public readonly ImmutableArray RequireItems; + private readonly ImmutableDictionary> OptionTargetItems; public bool HasOptionSpecificTargetItems => OptionTargetItems != null && OptionTargetItems.Any(); - public readonly string Identifier; - - private Color? color; + private readonly Color? color; public Color Color { get @@ -122,49 +71,39 @@ namespace Barotrauma { return color.Value; } - else if (Category.HasValue && OrderCategoryIcons.TryGetValue((OrderCategory)Category, out Tuple sprite)) + else if (OrderCategoryIcon.OrderCategoryIcons.ContainsKey(CategoryIdentifier)) { - return sprite.Item2; + return OrderCategoryIcon.OrderCategoryIcons[Category.ToIdentifier()].Color; } else { return Color.White; } } - private set - { - color = value; - } } - //if true, the order is issued to all available characters - public bool TargetAllCharacters { get; } + public readonly bool TargetAllCharacters; public bool IsReport => TargetAllCharacters && !MustSetTarget; + public bool IsDismissal => Identifier == DismissalIdentifier; + public readonly float FadeOutTime; - public Entity TargetEntity; - public ItemComponent TargetItemComponent; public readonly bool UseController; - public readonly string[] ControllerTags; - public Controller ConnectedController; - public Character OrderGiver; + public readonly ImmutableArray ControllerTags; - public OrderCategory? Category { get; private set; } - - //legacy support /// /// If defined, the order can only be quick-assigned to characters with these jobs. Or if it's a report, the icon will only be displayed to characters with these jobs. /// - public readonly string[] AppropriateJobs; - public readonly string[] Options; - public readonly string[] HiddenOptions; - public readonly string[] AllOptions; - private readonly Dictionary OptionNames; + public readonly ImmutableArray AppropriateJobs; + public readonly ImmutableArray Options; + public readonly ImmutableArray HiddenOptions; + public readonly ImmutableArray AllOptions; + public readonly ListDictionary OptionNames; - public readonly Dictionary OptionSprites; + public readonly ImmutableDictionary OptionSprites; public readonly bool MustSetTarget; /// @@ -172,40 +111,18 @@ namespace Barotrauma /// Note: if MustSetTarget is true, CanBeGeneralized will always be false. /// public readonly bool CanBeGeneralized; - public readonly string AppropriateSkill; + public readonly Identifier AppropriateSkill; public readonly bool Hidden; public readonly bool IgnoreAtOutpost; - public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1; - public bool IsPrefab { get; private set; } + public bool HasOptions => Options.Length > 1; public readonly bool MustManuallyAssign; public readonly bool AutoDismiss; + /// /// If defined, the order will be quick-assigned to characters with these jobs before characters with other jobs. /// - public string[] PreferredJobs { get; } - - public readonly OrderTarget TargetPosition; - - private ISpatialEntity targetSpatialEntity; - public ISpatialEntity TargetSpatialEntity - { - get - { - if (targetSpatialEntity == null) - { - if (TargetType == OrderTargetType.WallSection && WallSectionIndex.HasValue) - { - targetSpatialEntity = (TargetEntity as Structure)?.Sections[WallSectionIndex.Value]; - } - else - { - targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; - } - } - return targetSpatialEntity; - } - } + public readonly ImmutableArray PreferredJobs; public enum OrderTargetType { @@ -215,7 +132,7 @@ namespace Barotrauma } public OrderTargetType TargetType { get; } public int? WallSectionIndex { get; } - public bool IsIgnoreOrder { get; } + public bool IsIgnoreOrder => Identifier == "ignorethis" || Identifier == "unignorethis"; /// /// Should the order icon be drawn when the order target is inside a container @@ -231,88 +148,10 @@ namespace Barotrauma public bool ColoredWhenControllingGiver { get; } public bool DisplayGiverInTooltip { get; } - public static void Init() + public OrderPrefab(ContentXElement orderElement, OrdersFile file) : base(file, orderElement.GetAttributeIdentifier("identifier", "")) { - Prefabs = new Dictionary(); - OrderCategoryIcons = new Dictionary>(); - - foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.Orders)) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - bool allowOverriding = false; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - allowOverriding = true; - } - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; - string name = element.Name.ToString(); - if (name.Equals("order", StringComparison.OrdinalIgnoreCase)) - { - string identifier = element.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"Error in file {file.Path}: The order element '{name}' does not have an identifier! All orders must have a unique identifier."); - continue; - } - if (Prefabs.TryGetValue(identifier, out Order duplicate)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding an existing order '{identifier}' with another one defined in '{file.Path}'", Color.Yellow); - Prefabs.Remove(identifier); - } - else - { - DebugConsole.ThrowError($"Error in file {file.Path}: Duplicate element with the idenfitier '{identifier}' found in '{file.Path}'! All orders must have a unique identifier. Use tags to override an order with the same identifier."); - continue; - } - } - var newOrder = new Order(element); - newOrder.Prefab = newOrder; - Prefabs.Add(identifier, newOrder); - } - else if (name.Equals("ordercategory", StringComparison.OrdinalIgnoreCase)) - { - var category = (OrderCategory)Enum.Parse(typeof(OrderCategory), element.GetAttributeString("category", "undefined"), true); - if (OrderCategoryIcons.ContainsKey(category)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding an existing icon for the '{category}' order category with another one defined in '{file}'", Color.Yellow); - OrderCategoryIcons.Remove(category); - } - else - { - DebugConsole.ThrowError($"Error in file {file}: Duplicate element for the '{category}' order category found in '{file}'! All order categories must be unique. Use tags to override an order category."); - continue; - } - } - var spriteElement = element.GetChildElement("sprite"); - if (spriteElement != null) - { - var sprite = new Sprite(spriteElement, lazyLoad: true); - var color = element.GetAttributeColor("color", Color.White); - OrderCategoryIcons.Add(category, new Tuple(sprite, color)); - } - } - } - } - PrefabList = new List(Prefabs.Values); - } - - /// - /// Constructor for order prefabs - /// - private Order(XElement orderElement) - { - Identifier = orderElement.GetAttributeString("identifier", ""); - Name = TextManager.Get("OrderName." + Identifier, returnNull: true) ?? "Name not found"; - ContextualName = TextManager.Get("OrderNameContextual." + Identifier, returnNull: true) ?? Name; + Name = TextManager.Get($"OrderName.{Identifier}"); + ContextualName = TextManager.Get($"OrderNameContextual.{Identifier}"); string targetItemType = orderElement.GetAttributeString("targetitemtype", ""); if (!string.IsNullOrWhiteSpace(targetItemType)) @@ -331,15 +170,15 @@ namespace Barotrauma color = orderElement.GetAttributeColor("color"); FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f); UseController = orderElement.GetAttributeBool("usecontroller", false); - ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]); + ControllerTags = orderElement.GetAttributeIdentifierArray("controllertags", Array.Empty()).ToImmutableArray(); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); - AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); - PreferredJobs = orderElement.GetAttributeStringArray("preferredjobs", new string[0]); - Options = orderElement.GetAttributeStringArray("options", new string[0]); - HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]); - AllOptions = Options.Concat(HiddenOptions).ToArray(); - - OptionTargetItems = new Dictionary>(); + AppropriateJobs = orderElement.GetAttributeIdentifierArray("appropriatejobs", Array.Empty()).ToImmutableArray(); + PreferredJobs = orderElement.GetAttributeIdentifierArray("preferredjobs", Array.Empty()).ToImmutableArray(); + Options = orderElement.GetAttributeIdentifierArray("options", Array.Empty()).ToImmutableArray(); + HiddenOptions = orderElement.GetAttributeIdentifierArray("hiddenoptions", Array.Empty()).ToImmutableArray(); + AllOptions = Options.Concat(HiddenOptions).ToImmutableArray(); + + var optionTargetItems = new Dictionary>(); if (orderElement.GetAttributeString("targetitems", "") is string targetItems && targetItems.Contains(';')) { string[] splitTargetItems = targetItems.Split(';'); @@ -349,46 +188,38 @@ namespace Barotrauma DebugConsole.ThrowError($"Order \"{Identifier}\" has option-specific target items, but the option count doesn't match the target item count"); } #endif - var allTargetItems = new List(); + var allTargetItems = new List(); for (int i = 0; i < AllOptions.Length; i++) { - string[] optionTargetItems = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', ',') : new string[0]; - for (int j = 0; j < optionTargetItems.Length; j++) + Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', ',').ToIdentifiers() : Array.Empty(); + for (int j = 0; j < optionTargetItemsSplit.Length; j++) { - optionTargetItems[j] = optionTargetItems[j].ToLowerInvariant().Trim(); - allTargetItems.Add(optionTargetItems[j]); + optionTargetItemsSplit[j] = optionTargetItemsSplit[j].Value.Trim().ToIdentifier(); + allTargetItems.Add(optionTargetItemsSplit[j]); } - OptionTargetItems.Add(AllOptions[i], optionTargetItems.ToImmutableArray()); + optionTargetItems.Add(AllOptions[i], optionTargetItemsSplit.ToImmutableArray()); } TargetItems = allTargetItems.ToImmutableArray(); } else { - TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray(); + TargetItems = orderElement.GetAttributeIdentifierArray("targetitems", Array.Empty(), trim: true).ToImmutableArray(); } - RequireItems = orderElement.GetAttributeStringArray("requireitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray(); - + RequireItems = orderElement.GetAttributeIdentifierArray("requireitems", Array.Empty(), trim: true).ToImmutableArray(); + OptionTargetItems = optionTargetItems.ToImmutableDictionary(); + var category = orderElement.GetAttributeString("category", null); - if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } + this.Category = !string.IsNullOrWhiteSpace(category) ? Enum.Parse(category, true) : (OrderCategory?)null; + this.CategoryIdentifier = (this.Category?.ToString() ?? string.Empty).ToIdentifier(); MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); CanBeGeneralized = !MustSetTarget && orderElement.GetAttributeBool("canbegeneralized", true); - AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); + AppropriateSkill = orderElement.GetAttributeIdentifier("appropriateskill", Identifier.Empty); Hidden = orderElement.GetAttributeBool("hidden", false); IgnoreAtOutpost = orderElement.GetAttributeBool("ignoreatoutpost", false); - var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ?? - orderElement.GetAttributeStringArray("optionnames", new string[0]); - OptionNames = new Dictionary(); - for (int i = 0; i < Options.Length && i < optionNames.Length; i++) - { - OptionNames.Add(Options[i], optionNames[i].Trim()); - } - if (OptionNames.Count != Options.Length) - { - DebugConsole.AddWarning("Error in Order " + Name + " - the number of option names doesn't match the number of options."); - OptionNames.Clear(); - Options.ForEach(o => OptionNames.Add(o, o)); - } + OptionNames = + new ListDictionary( + TextManager.Get("OrderOptions." + Identifier).Split(',', ','), Options.Length, i => Options[i]); var spriteElement = orderElement.GetChildElement("sprite"); if (spriteElement != null) @@ -396,7 +227,7 @@ namespace Barotrauma SymbolSprite = new Sprite(spriteElement, lazyLoad: true); } - OptionSprites = new Dictionary(); + var optionSprites = new Dictionary(); if (Options != null && Options.Length > 0) { var optionSpriteElements = orderElement.GetChildElement("optionsprites")?.GetChildElements("sprite"); @@ -406,14 +237,13 @@ namespace Barotrauma { if (i >= optionSpriteElements.Count()) { break; }; var sprite = new Sprite(optionSpriteElements.ElementAt(i), lazyLoad: true); - OptionSprites.Add(Options[i], sprite); + optionSprites.Add(Options[i], sprite); } } } + OptionSprites = optionSprites.ToImmutableDictionary(); - IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); - IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Operate || Category == OrderCategory.Movement); AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100); @@ -421,92 +251,14 @@ namespace Barotrauma DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false); } - /// - /// Constructor for order instances - /// - public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null) + private bool HasSpecifiedJob(Character character, IReadOnlyList jobs) { - Prefab = prefab.Prefab ?? prefab; - - Name = prefab.Name; - ContextualName = prefab.ContextualName; - Identifier = prefab.Identifier; - ItemComponentType = prefab.ItemComponentType; - CanTypeBeSubclass = prefab.CanTypeBeSubclass; - TargetItems = prefab.TargetItems; - OptionTargetItems = prefab.OptionTargetItems; - RequireItems = prefab.RequireItems; - Options = prefab.Options; - SymbolSprite = prefab.SymbolSprite; - Color = prefab.Color; - UseController = prefab.UseController; - ControllerTags = prefab.ControllerTags; - TargetAllCharacters = prefab.TargetAllCharacters; - AppropriateJobs = prefab.AppropriateJobs; - PreferredJobs = prefab.PreferredJobs; - FadeOutTime = prefab.FadeOutTime; - MustSetTarget = prefab.MustSetTarget; - CanBeGeneralized = prefab.CanBeGeneralized; - AppropriateSkill = prefab.AppropriateSkill; - Category = prefab.Category; - MustManuallyAssign = prefab.MustManuallyAssign; - IsIgnoreOrder = prefab.IsIgnoreOrder; - DrawIconWhenContained = prefab.DrawIconWhenContained; - Hidden = prefab.Hidden; - IgnoreAtOutpost = prefab.IgnoreAtOutpost; - AssignmentPriority = prefab.AssignmentPriority; - AutoDismiss = prefab.AutoDismiss; - DisplayGiverInTooltip = prefab.DisplayGiverInTooltip; - ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver; - - OrderGiver = orderGiver; - TargetEntity = targetEntity; - if (targetItem != null) + if (jobs == null || jobs.Count == 0) { return false; } + Identifier jobIdentifier = character?.Info?.Job?.Prefab?.Identifier ?? Identifier.Empty; + if (jobIdentifier.IsEmpty) { return false; } + for (int i = 0; i < jobs.Count; i++) { - if (UseController) - { - ConnectedController = targetItem.Item?.FindController(tags: ControllerTags); - if (ConnectedController == null) - { - DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any."); - UseController = false; - } - } - TargetEntity = targetItem.Item; - TargetItemComponent = targetItem; - } - - TargetType = OrderTargetType.Entity; - - IsPrefab = false; - } - - /// - /// Constructor for order instances - /// - public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver) - { - TargetPosition = target; - TargetType = OrderTargetType.Position; - } - - /// - /// Constructor for order instances - /// - public Order(Order prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, targetEntity: wall, null, orderGiver: orderGiver) - { - WallSectionIndex = sectionIndex; - TargetType = OrderTargetType.WallSection; - } - - private bool HasSpecifiedJob(Character character, string[] jobs) - { - if (jobs == null || jobs.Length == 0) { return false; } - string jobIdentifier = character?.Info?.Job?.Prefab?.Identifier; - if (string.IsNullOrEmpty(jobIdentifier)) { return false; } - for (int i = 0; i < jobs.Length; i++) - { - if (jobIdentifier.Equals(jobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } + if (jobIdentifier == jobs[i]) { return true; } } return false; } @@ -515,14 +267,14 @@ namespace Barotrauma public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs); - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true) + public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) { if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed") { // Use special dialogue when we're rearranging character orders if (!givingOrderToSelf) { - return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty).Value; } else { @@ -531,7 +283,7 @@ namespace Barotrauma } } string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}"; - if (!string.IsNullOrEmpty(orderOption)) + if (!orderOption.IsEmpty) { if (Identifier != "dismissed") { @@ -539,19 +291,16 @@ namespace Barotrauma } else { - string[] splitOption = orderOption.Split('.'); + string[] splitOption = orderOption.Value.Split('.'); if (splitOption.Length > 0) { messageTag += $".{splitOption[0]}"; } } } - string msg = TextManager.GetWithVariables(messageTag, - new string[2] { "[name]", "[roomname]" }, - new string[2] { targetCharacterName ?? string.Empty, targetRoomName ?? string.Empty }, - formatCapitals: new bool[2] { false, true }, - returnNull: true); - return msg ?? string.Empty; + return TextManager.GetWithVariables(messageTag, + ("[name]", targetCharacterName ?? string.Empty, FormatCapitals.No), + ("[roomname]", targetRoomName ?? string.Empty, FormatCapitals.Yes)).Fallback("").Value; } /// @@ -578,7 +327,7 @@ namespace Barotrauma } /// Only returns items which are interactable for this character - public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null, string orderOption = null) + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null, Identifier orderOption = default) { List matchingItems = new List(); if (submarine == null) { return matchingItems; } @@ -604,7 +353,7 @@ namespace Barotrauma } /// Only returns items which are interactable for this character - public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null, string orderOption = null) + public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null, Identifier orderOption = default) { Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ? Submarine.MainSubs[1] : @@ -612,51 +361,41 @@ namespace Barotrauma return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor, orderOption: orderOption); } - public string GetOptionName(string id) + public LocalizedString GetOptionName(string id) { - if (Prefab == null) - { - if (OptionNames.ContainsKey(id)) { return OptionNames[id]; } - } - else - { - if (Prefab.OptionNames.ContainsKey(id)) { return Prefab.OptionNames[id]; } - } + return GetOptionName(id.ToIdentifier()); + } + + public LocalizedString GetOptionName(Identifier id) + { + if (OptionNames.ContainsKey(id)) { return OptionNames[id]; } return string.Empty; } - public string GetOptionName(int index) + public LocalizedString GetOptionName(int index) { if (index < 0 || index >= Options.Length) { return null; } - return GetOptionName(Options[index]); + return OptionNames[Options[index]]; } /// /// Used to create the order option for the Dismiss order to know which order it targets /// - /// The order to target with the dismiss order - public static string GetDismissOrderOption(OrderInfo orderInfo) + /// The order to target with the dismiss order + public static Identifier GetDismissOrderOption(Order order) { - if (orderInfo.Order != null) + Identifier option = order.Identifier; + if (order.Option != Identifier.Empty) { - string option = orderInfo.Order.Identifier; - if (!string.IsNullOrEmpty(orderInfo.OrderOption)) - { - option += $".{orderInfo.OrderOption}"; - } - return option; + option = $"{option}.{order.Option}".ToIdentifier(); } - return ""; + return option; } - public override string ToString() + + public ImmutableArray GetTargetItems(Identifier option = default) { - return $"Order ({Name})"; - } - - public ImmutableArray GetTargetItems(string option = null) - { - if (string.IsNullOrEmpty(option) || !OptionTargetItems.TryGetValue(option, out ImmutableArray optionTargetItems)) + if (option.IsEmpty || !OptionTargetItems.TryGetValue(option, out ImmutableArray optionTargetItems)) { return TargetItems; } @@ -666,16 +405,380 @@ namespace Barotrauma } } - public bool TargetItemsMatchItem(Item item, string option = null) + public bool TargetItemsMatchItem(Item item, Identifier option = default) { if (item == null) { return false; } - ImmutableArray targetItems = GetTargetItems(option); + ImmutableArray targetItems = GetTargetItems(option); return TargetItemsMatchItem(targetItems, item); } - public static bool TargetItemsMatchItem(ImmutableArray targetItems, Item item) + public static bool TargetItemsMatchItem(ImmutableArray targetItems, Item item) { return item != null && targetItems != null && targetItems.Length > 0 && (targetItems.Contains(item.Prefab.Identifier) || item.HasTag(targetItems)); } + + public override void Dispose() { } + } + + class Order + { + public readonly OrderPrefab Prefab; + public readonly Identifier Option; + public readonly int ManualPriority; + public readonly OrderType Type; + public readonly AIObjective Objective; + public bool IsCurrentOrder => Type == OrderType.Current; + public bool IsDismissal => Prefab.IsDismissal; + + public enum OrderType + { + Current, + Previous + } + + public readonly Entity TargetEntity; + public readonly ItemComponent TargetItemComponent; + public readonly Controller ConnectedController; + + public readonly Character OrderGiver; + + public readonly OrderTarget TargetPosition; + + private ISpatialEntity targetSpatialEntity; + public ISpatialEntity TargetSpatialEntity + { + get + { + if (targetSpatialEntity == null) + { + if (TargetType == OrderTargetType.WallSection && WallSectionIndex.HasValue) + { + targetSpatialEntity = (TargetEntity as Structure)?.Sections[WallSectionIndex.Value]; + } + else + { + targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; + } + } + return targetSpatialEntity; + } + } + + public Hull TargetHull => TargetEntity as Hull; + + public enum OrderTargetType + { + Entity, + Position, + WallSection + } + public readonly OrderTargetType TargetType; + public readonly int? WallSectionIndex; + + public LocalizedString Name => Prefab.Name; + public LocalizedString ContextualName => Prefab.ContextualName; + public Identifier Identifier => Prefab.Identifier; + public Type ItemComponentType => Prefab.ItemComponentType; + public bool CanTypeBeSubclass => Prefab.CanTypeBeSubclass; + public ref readonly ImmutableArray ControllerTags => ref Prefab.ControllerTags; + public ref readonly ImmutableArray TargetItems => ref Prefab.TargetItems; + public ref readonly ImmutableArray RequireItems => ref Prefab.RequireItems; + public ref readonly ImmutableArray Options => ref Prefab.Options; + public ref readonly ImmutableArray HiddenOptions => ref Prefab.HiddenOptions; + public ref readonly ImmutableArray AllOptions => ref Prefab.AllOptions; + public Sprite SymbolSprite => Prefab.SymbolSprite; + public Color Color => Prefab.Color; + public bool TargetAllCharacters => Prefab.TargetAllCharacters; + public ref readonly ImmutableArray AppropriateJobs => ref Prefab.AppropriateJobs; + public float FadeOutTime => Prefab.FadeOutTime; + public bool MustSetTarget => Prefab.MustSetTarget; + public Identifier AppropriateSkill => Prefab.AppropriateSkill; + public OrderCategory? Category => Prefab.Category; + public bool MustManuallyAssign => Prefab.MustManuallyAssign; + public bool IsIgnoreOrder => Prefab.IsIgnoreOrder; + public bool DrawIconWhenContained => Prefab.DrawIconWhenContained; + public bool Hidden => Prefab.Hidden; + public bool IgnoreAtOutpost => Prefab.IgnoreAtOutpost; + public bool IsReport => Prefab.IsReport; + public bool AutoDismiss => Prefab.AutoDismiss; + public int AssignmentPriority => Prefab.AssignmentPriority; + + public bool ColoredWhenControllingGiver => Prefab.ColoredWhenControllingGiver; + public bool DisplayGiverInTooltip => Prefab.DisplayGiverInTooltip; + + + public readonly bool UseController; + + /// + /// Constructor for order instances + /// + public Order(OrderPrefab prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + : this(prefab, Identifier.Empty, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } + + + public Order(OrderPrefab prefab, Identifier option, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + : this(prefab, option, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } + + public Order(OrderPrefab prefab, OrderTarget target, Character orderGiver = null) + : this(prefab, prefab.Options.FirstOrDefault(), 0, OrderType.Current, null, target, orderGiver) { } + + public Order(OrderPrefab prefab, Identifier option, OrderTarget target, Character orderGiver = null) + : this(prefab, option, 0, OrderType.Current, null, target, orderGiver) { } + + public Order(OrderPrefab prefab, Structure wall, int? sectionIndex, Character orderGiver = null) + : this(prefab, Identifier.Empty, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } + + public Order(OrderPrefab prefab, Identifier option, Structure wall, int? sectionIndex, Character orderGiver = null) + : this(prefab, option, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } + + public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + { + Prefab = prefab; + Option = option; + ManualPriority = manualPriority; + Type = orderType; + Objective = aiObjective; + + UseController = Prefab.UseController; + + OrderGiver = orderGiver; + TargetEntity = targetEntity; + if (targetItem != null) + { + if (UseController) + { + ConnectedController = targetItem.Item?.FindController(tags: ControllerTags); + if (ConnectedController == null) + { + DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any."); + UseController = false; + } + } + TargetEntity = targetItem.Item; + TargetItemComponent = targetItem; + } + + TargetType = OrderTargetType.Entity; + } + + public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null) + : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: null, targetItem: null, orderGiver) + { + TargetPosition = target; + TargetType = OrderTargetType.Position; + } + + public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null) + : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: wall, null, orderGiver: orderGiver) + { + WallSectionIndex = sectionIndex; + TargetType = OrderTargetType.WallSection; + } + + private Order( + Order other, + OrderPrefab prefab = null, + Identifier option = default, + int? manualPriority = null, + OrderType? type = null, + AIObjective objective = null, + Entity targetEntity = null, + ItemComponent targetItemComponent = null, + Controller connectedController = null, + Character orderGiver = null, + OrderTarget targetPosition = null, + OrderTargetType? targetType = null, + int? wallSectionIndex = null, + bool? useController = null) + { + Prefab = prefab ?? other.Prefab; + Option = option.IfEmpty(other.Option); + ManualPriority = manualPriority ?? other.ManualPriority; + Type = type ?? other.Type; + Objective = objective ?? other.Objective; + + TargetEntity = targetEntity ?? other.TargetEntity; + TargetItemComponent = targetItemComponent ?? other.TargetItemComponent; + ConnectedController = connectedController ?? other.ConnectedController; + + OrderGiver = orderGiver ?? other.OrderGiver; + + TargetPosition = targetPosition ?? other.TargetPosition; + + TargetType = targetType ?? other.TargetType; + WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex; + + UseController = useController ?? other.UseController; + } + + public Order WithOption(Identifier option) + { + return new Order(this, option: option); + } + + public Order WithManualPriority(int newPriority) + { + return new Order(this, manualPriority: newPriority); + } + + public Order WithOrderGiver(Character orderGiver) + { + return new Order(this, orderGiver: orderGiver); + } + + public Order WithObjective(AIObjective objective) + { + return new Order(this, objective: objective); + } + + public Order WithTargetEntity(Entity entity) + { + return new Order(this, targetEntity: entity); + } + + public Order WithTargetSpatialEntity(ISpatialEntity spatialEntity) + { + if (spatialEntity is WallSection wallSection) + { + Structure wall = wallSection.Wall; + int sectionIndex = wall.Sections.IndexOf(wallSection); + return WithWallSection(wall, sectionIndex); + } + else if (spatialEntity is Entity entity) + { + return WithTargetEntity(entity); + } + else if (spatialEntity is OrderTarget orderTarget) + { + return WithTargetPosition(orderTarget); + } + + throw new InvalidOperationException($"Unexpected input type: {spatialEntity.GetType().Name}"); + } + + public Order WithItemComponent(Item item, ItemComponent component = null) + { + return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item)); + } + + public Order WithWallSection(Structure wall, int? sectionIndex) + { + return new Order(this, targetEntity: wall, wallSectionIndex: sectionIndex, targetType: OrderTargetType.WallSection); + } + + public Order WithType(OrderType type) + { + return new Order(this, type: type); + } + + public Order WithTargetPosition(OrderTarget targetPosition) + { + return new Order(this, targetPosition: targetPosition); + } + + public Order Clone() + { + return new Order(this); + } + + public Order GetDismissal() + { + if (IsDismissal) { throw new InvalidOperationException("Attempted to dismiss a dismissal order"); } + return new Order(this, prefab: OrderPrefab.Prefabs["dismissed"], option: GetDismissOrderOption(this)); + } + + public bool HasAppropriateJob(Character character) + => Prefab.HasAppropriateJob(character); + + public bool HasPreferredJob(Character character) + => Prefab.HasPreferredJob(character); + + public string GetChatMessage( + string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) + => Prefab.GetChatMessage(targetCharacterName, targetRoomName, givingOrderToSelf, orderOption, isNewOrder); + + /// + /// Get the target item component based on the target item type + /// + public ItemComponent GetTargetItemComponent(Item item) + { + return Prefab.GetTargetItemComponent(item); + } + + public bool TryGetTargetItemComponent(Item item, out ItemComponent firstMatchingComponent) + { + return Prefab.TryGetTargetItemComponent(item, out firstMatchingComponent); + } + + /// Only returns items which are interactable for this character + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null) + { + return Prefab.GetMatchingItems(submarine, mustBelongToPlayerSub, requiredTeam, interactableFor); + } + + + /// Only returns items which are interactable for this character + public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null) + { + return Prefab.GetMatchingItems(mustBelongToPlayerSub, interactableFor); + } + + public LocalizedString GetOptionName(string id) + { + return Prefab.GetOptionName(id); + } + + public LocalizedString GetOptionName(Identifier id) + { + return Prefab.GetOptionName(id); + } + + public LocalizedString GetOptionName(int index) + { + return Prefab.GetOptionName(index); + } + + /// + /// Used to create the order option for the Dismiss order to know which order it targets + /// + /// The order to target with the dismiss order + public static Identifier GetDismissOrderOption(Order order) + { + return OrderPrefab.GetDismissOrderOption(order); + } + + public bool MatchesOrder(Identifier orderIdentifier, Identifier orderOption) => + orderIdentifier == Identifier && orderOption == Option; + + /*public bool MatchesOrder(Order order, Identifier option) => + order != null && MatchesOrder(order.Identifier, option);*/ + + public bool MatchesOrder(Order order) => + order != null && MatchesOrder(order.Identifier, order.Option); + + public bool MatchesDismissedOrder(Identifier dismissOrderOption) + { + Identifier[] dismissedOrder = dismissOrderOption.Value.Split('.').Select(s => s.ToIdentifier()).ToArray(); + if (dismissedOrder != null && dismissedOrder.Length > 0) + { + Identifier dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : Identifier.Empty; + if (dismissedOrderIdentifier == Identifier.Empty || dismissedOrderIdentifier != Identifier) { return false; } + Identifier dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : Identifier.Empty; + if (dismissedOrderOption == Identifier.Empty && Option == Identifier.Empty) { return true; } + return dismissedOrderOption == Option; + } + else + { + return false; + } + } + + public ImmutableArray GetTargetItems(Identifier option = default) + => Prefab.GetTargetItems(option); + + public override string ToString() + { + return $"Order ({Name})"; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 079b59da6..1105eab19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -93,15 +93,15 @@ namespace Barotrauma } Rate = element.GetAttributeFloat("rate", 0.016f); totalCommonness = 0.0f; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { case "item": - string identifier = subElement.GetAttributeString("identifier", ""); + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); Item newItemToProduce = new Item { - Prefab = string.IsNullOrEmpty(identifier) ? null : ItemPrefab.Find("", subElement.GetAttributeString("identifier", "")), + Prefab = identifier.IsEmpty ? null : ItemPrefab.Find("", subElement.GetAttributeIdentifier("identifier", Identifier.Empty)), Commonness = subElement.GetAttributeFloat("commonness", 0.0f) }; totalCommonness += newItemToProduce.Commonness; @@ -134,8 +134,8 @@ namespace Barotrauma aggregate += Items[i].Commonness; if (aggregate >= r && Items[i].Prefab != null) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); - Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); + Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); break; } } @@ -174,7 +174,7 @@ namespace Barotrauma PlayForce = element.GetAttributeFloat("playforce", 15.0f); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { @@ -202,7 +202,7 @@ namespace Barotrauma } } - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName); } public StatusIndicatorType GetCurrentStatusIndicatorType() @@ -218,7 +218,7 @@ namespace Barotrauma bool success = OnEat(item.GetTags()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier); } return success; } @@ -226,28 +226,28 @@ namespace Barotrauma public bool OnEat(Character character) { if (character == null || !character.IsDead) { return false; } - bool success = OnEat("dead"); + bool success = OnEat("dead".ToIdentifier()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); } return success; } - private bool OnEat(IEnumerable tags) + private bool OnEat(IEnumerable tags) { - foreach (string tag in tags) + foreach (Identifier tag in tags) { if (OnEat(tag)) { return true; } } return false; } - private bool OnEat(string tag) + public bool OnEat(Identifier tag) { for (int i = 0; i < foods.Count; i++) { - if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase)) + if (tag == foods[i].Tag) { Hunger += foods[i].Hunger; Happiness += foods[i].Happiness; @@ -352,7 +352,7 @@ namespace Barotrauma } else if (Hunger < MaxHunger * 0.1f) { - character.CharacterHealth.ReduceAffliction(null, null, 8.0f * deltaTime); + character.CharacterHealth.ReduceAllAfflictionsOnAllLimbs(8.0f * deltaTime); } if (character.SelectedBy != null) @@ -404,7 +404,7 @@ namespace Barotrauma public static void LoadPets(XElement petsElement) { - foreach (XElement subElement in petsElement.Elements()) + foreach (var subElement in petsElement.Elements()) { string speciesName = subElement.GetAttributeString("speciesname", ""); string seed = subElement.GetAttributeString("seed", "123"); @@ -418,9 +418,9 @@ namespace Barotrauma else { //try to find a spawnpoint in the main sub - var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandom(); + var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced(); //if not found, try any player sub (shuttle/drone etc) - spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom(); + spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } var pet = Character.Create(speciesName, spawnPos, seed); @@ -439,7 +439,7 @@ namespace Barotrauma var inventoryElement = subElement.Element("inventory"); if (inventoryElement != null) { - pet.SpawnInventoryItems(pet.Inventory, inventoryElement); + pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 8aca8933a..95ff37b9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public const float MaxImportance = 100f; public const float MinImportance = 0f; - public Order SuggestedOrderPrefab { get; } + public Order SuggestedOrder { get; } private float importance; public float Importance @@ -25,11 +25,11 @@ namespace Barotrauma public float CurrentRedundancy { get; set; } public readonly ShipCommandManager shipCommandManager; - public string Option { get; set; } + public Identifier Option => SuggestedOrder.Option; public Character OrderedCharacter { get; set; } public Order CurrentOrder { get; private set; } - public ItemComponent TargetItemComponent { get; protected set; } - public Item TargetItem { get; protected set; } + public ItemComponent TargetItemComponent => SuggestedOrder.TargetItemComponent; + public Item TargetItem => SuggestedOrder.TargetEntity as Item; public bool Active { get; protected set; } = true; // used to turn off the instance if errors are detected protected virtual Character CommandingCharacter => shipCommandManager.character; @@ -38,25 +38,28 @@ namespace Barotrauma public virtual bool StopDuringEmergency => true; // limit certain issue assessments when invaded by the enemies public virtual bool AllowEasySwitching => false; - public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, string option = null) + public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrder) { this.shipCommandManager = shipCommandManager; - SuggestedOrderPrefab = suggestedOrderPrefab; - Option = option; + SuggestedOrder = suggestedOrder; } public void SetOrder(Character orderedCharacter) { OrderedCharacter = orderedCharacter; - if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrderPrefab, Option))) + if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option))) { if (orderedCharacter != CommandingCharacter) { - CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); + CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); } - CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter); - OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter); - OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5); + CurrentOrder = SuggestedOrder + .WithOption(Option) + .WithItemComponent(TargetItem, TargetItemComponent) + .WithOrderGiver(CommandingCharacter) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter); + OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5); } TimeSinceLastAttempt = 0f; } @@ -113,7 +116,7 @@ namespace Barotrauma } // accept only the highest priority order - if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder) + if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder) { #if DEBUG ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs index f588de938..501ac74ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs @@ -4,11 +4,7 @@ namespace Barotrauma { abstract class ShipIssueWorkerItem : ShipIssueWorker { - public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option = null) : base(shipCommandManager, order, option) - { - TargetItemComponent = targetItemComponent; - TargetItem = targetItem; - } + public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } protected override bool IsIssueViable() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index c227317ca..d4d1ad1ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -12,7 +12,7 @@ namespace Barotrauma public override bool AllowEasySwitching => true; - public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent) : base(shipCommandManager, order, targetItem, targetItemComponent) { } + public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } float GetTargetingImportance(Entity entity) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs index 9f6cbe6ec..9dc7dc398 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs @@ -4,9 +4,7 @@ namespace Barotrauma { class ShipIssueWorkerPowerUpReactor : ShipIssueWorkerItem { - public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) - { - } + public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } public override void CalculateImportanceSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs index e69eba53f..aa6328e96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs @@ -7,7 +7,7 @@ namespace Barotrauma // The AI could be set to steer automatically through a specialized job or autonomous objectives // but the logic involved doesn't really allow that without some annoyingly specific changes // hence the AI will command itself to steer if steering is not being taken care of or the target location is wrong - public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) { } + public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } public override void CalculateImportanceSpecific() { if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 2cc21e90e..d171db72a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -95,7 +95,7 @@ namespace Barotrauma public static void ShipCommandLog(string text) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage(text); } @@ -251,14 +251,14 @@ namespace Barotrauma if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold) { - IEnumerable bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrderPrefab, AlliedCharacters, character, true); + IEnumerable bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true); foreach (Character orderedCharacter in bestCharacters) { float issueApplicability = mostImportantIssue.Importance; // prefer not to switch if not qualified - issueApplicability *= mostImportantIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f; + issueApplicability *= mostImportantIssue.SuggestedOrder.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f; ShipIssueWorker occupiedIssue = attendedIssues.FirstOrDefault(i => i.OrderedCharacter == orderedCharacter); @@ -276,7 +276,7 @@ namespace Barotrauma } // give slight preference if not qualified for current job - issueApplicability += occupiedIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f; + issueApplicability += occupiedIssue.SuggestedOrder.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f; // prefer not to switch orders unless considerably more important issueApplicability -= IssueDevotionBuffer; @@ -312,9 +312,9 @@ namespace Barotrauma #if DEBUG ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter); #endif - Order orderPrefab = Order.GetPrefab("dismissed"); + var order = new Order(OrderPrefab.Dismissal, null).WithManualPriority(3).WithOrderGiver(character); //character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false)); - shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character); + shipIssueWorker.OrderedCharacter.SetOrder(order); shipIssueWorker.RemoveOrder(); break; } @@ -346,18 +346,21 @@ namespace Barotrauma if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { - ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup")); + var order = new Order(OrderPrefab.Prefabs["operatereactor"], "powerup".ToIdentifier(), reactor.Item, reactor); + ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, order)); } if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent() is Steering steeringComponent) { steering = steeringComponent; - ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, Order.GetPrefab("steer"), nav, steeringComponent, "navigatetactical")); + var order = new Order(OrderPrefab.Prefabs["steer"], "navigatetactical".ToIdentifier(), nav, steeringComponent); + ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order)); } foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret"))) { - ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, Order.GetPrefab("operateweapons"), item, item.GetComponent())); + var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent()); + ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); } int crewSizeModifier = 2; @@ -365,14 +368,16 @@ namespace Barotrauma ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this); for (int i = 0; i < crewSizeModifier; i++) { - ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, Order.GetPrefab("fixleaks"), shipGlobalIssueFixLeaks)); + var order = new Order(OrderPrefab.Prefabs["fixleaks"], null); + ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, order, shipGlobalIssueFixLeaks)); } shipGlobalIssues.Add(shipGlobalIssueFixLeaks); ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this); for (int i = 0; i < crewSizeModifier; i++) { - ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, Order.GetPrefab("repairsystems"), shipGlobalIssueRepairSystems)); + var order = new Order(OrderPrefab.Prefabs["repairsystems"], null); + ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, order, shipGlobalIssueRepairSystems)); } shipGlobalIssues.Add(shipGlobalIssueRepairSystems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 3fe39c57b..9de13ceed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -31,11 +31,11 @@ namespace Barotrauma private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity); - private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); + private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); - private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag)); + private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); - private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); + private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); public static WreckAI Create(Submarine wreck) { @@ -54,14 +54,14 @@ namespace Barotrauma return; } var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); - var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.Server); + var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient); if (brainPrefab == null) { DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); return; } allItems = Wreck.GetItems(false); - thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab)); + thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab)); hulls.AddRange(Wreck.GetHulls(false)); var potentialBrainHulls = new List<(Hull hull, float weight)>(); brain = new Item(brainPrefab, Vector2.Zero, Wreck); @@ -103,12 +103,12 @@ namespace Barotrauma potentialBrainHulls.Add((hull, weight)); } } - Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.Server); - var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p)); + Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.ServerAndClient); + var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(IsThalamus); if (brainHull == null) { DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room."); - brainHull = hulls.GetRandom(Rand.RandSync.Server); + brainHull = hulls.GetRandom(Rand.RandSync.ServerAndClient); } if (brainHull == null) { @@ -118,12 +118,12 @@ namespace Barotrauma brainHull.WaterVolume = brainHull.Volume; brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); brain.CurrentHull = brainHull; - var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.Server); + var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient); if (backgroundPrefab != null) { new Structure(brainHull.Rect, backgroundPrefab, Wreck); } - var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.Server); + var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient); if (horizontalWallPrefab != null) { int height = (int)horizontalWallPrefab.Size.Y; @@ -132,7 +132,7 @@ namespace Barotrauma new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); } - var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.Server); + var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient); if (verticalWallPrefab != null) { int width = (int)verticalWallPrefab.Size.X; @@ -162,7 +162,7 @@ namespace Barotrauma { if (container.Inventory.GetItemAt(i) != null) { continue; } if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && - Config.ForbiddenAmmunition.None(id => id.Equals(ip.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) + Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) { Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) @@ -272,7 +272,7 @@ namespace Barotrauma cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside); for (int i = 0; i < cellsOutside; i++) { - ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null); + ISpatialEntity targetEntity = wayPoints.GetRandomUnsynced(wp => wp.CurrentHull == null); if (targetEntity == null) { break; } if (!TrySpawnCell(out _, targetEntity)) { break; } } @@ -310,7 +310,7 @@ namespace Barotrauma // but as long as spawning is handled via status effects, I don't know if there is any better way. // In practice there shouldn't be terminal cells from different thalamus organisms at the same time. // And if there was, the distance check should prevent killing the agents of a different organism. - if (character.SpeciesName.Equals(Config.OffensiveAgent, StringComparison.OrdinalIgnoreCase)) + if (character.SpeciesName == Config.OffensiveAgent) { // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. float maxDistance = Sonar.DefaultSonarRange; @@ -341,7 +341,7 @@ namespace Barotrauma public static void RemoveThalamusItems(Submarine wreck) { List thalamusItems = new List(); - foreach (var wreckAiConfig in WreckAIConfig.List) + foreach (var wreckAiConfig in WreckAIConfig.Prefabs) { thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity)); } @@ -391,7 +391,7 @@ namespace Barotrauma cellSpawnTimer -= deltaTime; if (cellSpawnTimer < 0) { - TrySpawnCell(out _, spawnOrgans.GetRandom()); + TrySpawnCell(out _, spawnOrgans.GetRandomUnsynced()); cellSpawnTimer = GetSpawnTime(); } } @@ -403,8 +403,8 @@ namespace Barotrauma if (targetEntity == null) { targetEntity = - wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ?? - hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity; + wayPoints.GetRandomUnsynced(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ?? + hulls.GetRandomUnsynced(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity; } if (targetEntity == null) { return false; } if (targetEntity is Hull h) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index e2a9c9b0a..92634f40a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using System; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -6,131 +7,97 @@ using System.Xml.Linq; namespace Barotrauma { - class WreckAIConfig : ISerializableEntity + class WreckAIConfig : PrefabWithUintIdentifier, ISerializableEntity { + public readonly static PrefabCollection Prefabs = new PrefabCollection(); + public string Name => "Wreck AI Config"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - [Serialize("", false)] - public string Entity { get; private set; } + public Identifier Entity => Identifier; - [Serialize("", false)] - public string DefensiveAgent { get; private set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier DefensiveAgent { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string OffensiveAgent { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string Brain { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string Spawner { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string BrainRoomBackground { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string BrainRoomVerticalWall { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string BrainRoomHorizontalWall { get; private set; } - [Serialize(60f, false)] + [Serialize(60f, IsPropertySaveable.No)] public float AgentSpawnDelay { get; private set; } - [Serialize(0.5f, false)] + [Serialize(0.5f, IsPropertySaveable.No)] public float AgentSpawnDelayRandomFactor { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AgentSpawnDelayDifficultyMultiplier { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AgentSpawnCountDifficultyMultiplier { get; private set; } - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int MinAgentsPerBrainRoom { get; private set; } - [Serialize(3, false)] + [Serialize(3, IsPropertySaveable.No)] public int MaxAgentsPerRoom { get; private set; } - [Serialize(2, false)] + [Serialize(2, IsPropertySaveable.No)] public int MinAgentsOutside { get; private set; } - [Serialize(5, false)] + [Serialize(5, IsPropertySaveable.No)] public int MaxAgentsOutside { get; private set; } - [Serialize(3, false)] + [Serialize(3, IsPropertySaveable.No)] public int MinAgentsInside { get; private set; } - [Serialize(10, false)] + [Serialize(10, IsPropertySaveable.No)] public int MaxAgentsInside { get; private set; } - [Serialize(15, false)] + [Serialize(15, IsPropertySaveable.No)] public int MaxAgentCount { get; private set; } - [Serialize(100f, false)] + [Serialize(100f, IsPropertySaveable.No)] public float MinWaterLevel { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool KillAgentsWhenEntityDies { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float DeadEntityColorMultiplier { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float DeadEntityColorFadeOutTime { get; private set; } - public readonly string[] ForbiddenAmmunition; + public readonly Identifier[] ForbiddenAmmunition; - public static List List + public static WreckAIConfig GetRandom() => Prefabs.GetRandom(Rand.RandSync.ServerAndClient); + + protected override Identifier DetermineIdentifier(XElement element) { - get - { - if (paramsList == null) - { - LoadAll(); - } - return paramsList; - } + return element.GetAttributeIdentifier("Entity", base.DetermineIdentifier(element)); } - private static List paramsList; - - public static WreckAIConfig GetRandom() => List.GetRandom(Rand.RandSync.Server); - - public WreckAIConfig(XElement element) + public WreckAIConfig(ContentXElement element, WreckAIConfigFile file) : base(file, element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - ForbiddenAmmunition = XMLExtensions.GetAttributeStringArray(element, "ForbiddenAmmunition", new string[0], convertToLowerInvariant: true); + ForbiddenAmmunition = XMLExtensions.GetAttributeIdentifierArray(element, "ForbiddenAmmunition", Array.Empty()); } - public static void LoadAll() - { - paramsList = new List(); - var files = GameMain.Instance.GetFilesOfType(ContentType.WreckAIConfig); - if (files.None()) - { - DebugConsole.ThrowError("Cannot find any Wreck AI config!"); - return; - } - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (mainElement.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - paramsList.Clear(); - DebugConsole.NewMessage($"Overriding the wreck ai config with '{file.Path}'", Color.Yellow); - } - else if (paramsList.Any()) - { - DebugConsole.NewMessage($"Adding additional wreck ai config from file '{file.Path}'"); - } - paramsList.Add(new WreckAIConfig(mainElement)); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 6904391ca..7981478e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -11,8 +11,8 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) - : base(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) + public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) + : base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) { InitProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs index 3880a0dea..967f1de57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs @@ -13,14 +13,14 @@ namespace Barotrauma /// An arbitrary identifier that can be used to determine what kind of a message this is /// and prevent characters from saying the same kind of line too often. /// - public readonly string Identifier; + public readonly Identifier Identifier; public ChatMessageType? MessageType; public float SendDelay; public double SendTime; - public AIChatMessage(string message, ChatMessageType? type, string identifier = "", float delay = 0.0f) + public AIChatMessage(string message, ChatMessageType? type, Identifier identifier = default, float delay = 0.0f) { Message = message; MessageType = type; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 074817047..50af42736 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -22,8 +22,9 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName); - if (character.VariantOf != null) + #warning TODO: this is kinda janky, this should probably be done better + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName)); + if (!character.VariantOf.IsEmpty) { _ragdollParams.ApplyVariantScale(character.Params.VariantFile); } @@ -421,7 +422,7 @@ namespace Barotrauma } //only one limb left, the character is now full eaten - Entity.Spawner?.AddToRemoveQueue(target); + Entity.Spawner?.AddEntityToRemoveQueue(target); if (Character.AIController is EnemyAIController enemyAi) { @@ -432,10 +433,10 @@ namespace Barotrauma } else //sever a random joint { - target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom()); + target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandomUnsynced()); } } - } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index a02fefc3c..c82919b64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -25,7 +25,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName); + _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.SpeciesName); } return _ragdollParams; } @@ -178,6 +178,14 @@ namespace Barotrauma else if (Crouching) { shoulderHeight -= 0.15f; + if (Crouching) + { + bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f); + if (!movingHorizontally) + { + shoulderHeight -= HumanCrouchParams.MoveDownAmountWhenStationary; + } + } } return Collider.SimPosition + new Vector2( @@ -1401,8 +1409,8 @@ namespace Barotrauma else { //stabilize the oxygen level but don't allow it to go positive and revive the character yet - float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill; - stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax); + float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill; + stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax); character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we } @@ -1426,23 +1434,23 @@ namespace Barotrauma targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); cprPump = 0; - if (skill < CPRSettings.DamageSkillThreshold) + if (skill < CPRSettings.Active.DamageSkillThreshold) { target.LastDamageSource = null; target.DamageLimb( targetTorso.WorldPosition, targetTorso, - new[] { CPRSettings.InsufficientSkillAffliction.Instantiate((CPRSettings.DamageSkillThreshold - skill) * CPRSettings.DamageSkillMultiplier, source: character) }, + new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) }, 0.0f, true, 0.0f, attacker: null); } if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code { - float reviveChance = skill * CPRSettings.ReviveChancePerSkill; - reviveChance = (float)Math.Pow(reviveChance, CPRSettings.ReviveChanceExponent); - reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.ReviveChanceMin, CPRSettings.ReviveChanceMax); + float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill; + reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); + reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); if (powerfulCPR) { reviveChance *= 2.0f; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) <= reviveChance) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) { //increase oxygen and clamp it above zero // -> the character should be revived if there are no major afflictions in addition to lack of oxygen @@ -1463,7 +1471,7 @@ namespace Barotrauma target.CharacterHealth.CalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { - character.Info?.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive); + character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive); SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 2dc2c80e7..39fd2305e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -68,7 +68,7 @@ namespace Barotrauma "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } - return new Limb[0]; + return Array.Empty(); } return limbs; } @@ -423,13 +423,13 @@ namespace Barotrauma #endif var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath); - if (characterPrefab?.XDocument != null) + if (characterPrefab?.ConfigElement != null) { - var mainElement = characterPrefab.XDocument.Root.IsOverride() ? characterPrefab.XDocument.Root.FirstElement() : characterPrefab.XDocument.Root; + var mainElement = characterPrefab.ConfigElement; foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } - AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this); + AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty), huskAppendage, ragdoll: this); } } } @@ -1408,7 +1408,7 @@ namespace Barotrauma #else DebugConsole.NewMessage(errorMsg.Replace("[name]", Character.Name), Color.Red); #endif - GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName.Value)); if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 18b72752a..080c14292 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -77,32 +77,32 @@ namespace Barotrauma partial class Attack : ISerializableEntity { - [Serialize(AttackContext.Any, true, description: "The attack will be used only in this context."), Editable] + [Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable] public AttackContext Context { get; private set; } - [Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable] + [Serialize(AttackTarget.Any, IsPropertySaveable.Yes, description: "Does the attack target only specific targets?"), Editable] public AttackTarget TargetType { get; private set; } - [Serialize(LimbType.None, true, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable] public LimbType TargetLimbType { get; private set; } - [Serialize(HitDetection.Distance, true, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable] + [Serialize(HitDetection.Distance, IsPropertySaveable.Yes, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable] public HitDetection HitDetectionType { get; private set; } - [Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable] + [Serialize(AIBehaviorAfterAttack.FallBack, IsPropertySaveable.Yes, description: "The preferred AI behavior after the attack."), Editable] public AIBehaviorAfterAttack AfterAttack { get; set; } - [Serialize(0f, true, description: "A delay before reacting after performing an attack."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "A delay before reacting after performing an attack."), Editable] public float AfterAttackDelay { get; set; } - [Serialize(false, true, description: "Should the AI try to turn around when aiming with this attack?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable] public bool Reverse { get; private set; } - [Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] public bool Retreat { get; private set; } private float _range; - [Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] public float Range { get => _range * RangeMultiplier; @@ -110,48 +110,48 @@ namespace Barotrauma } private float _damageRange; - [Serialize(0.0f, true, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] public float DamageRange { get => _damageRange * RangeMultiplier; set => _damageRange = value; } - [Serialize(0.25f, true, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } - [Serialize(5f, true, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float CoolDown { get; set; } = 5; - [Serialize(0f, true, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float SecondaryCoolDown { get; set; } = 0; - [Serialize(0f, true, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float CoolDownRandomFactor { get; private set; } = 0; - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool FullSpeedAfterAttack { get; private set; } private float _structureDamage; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float StructureDamage { get => _structureDamage * DamageMultiplier; set => _structureDamage = value; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool EmitStructureDamageParticles { get; private set; } private float _itemDamage; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float ItemDamage { get =>_itemDamage * DamageMultiplier; set => _itemDamage = value; } - [Serialize(0.0f, true, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] public float Penetration { get; private set; } /// @@ -169,28 +169,28 @@ namespace Barotrauma /// public float ImpactMultiplier { get; set; } = 1; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool Ranged { get; set; } - [Serialize(false, true, description:"Only affects ranged attacks.")] + [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks.")] public bool AvoidFriendlyFire { get; set; } - [Serialize(20f, true)] + [Serialize(20f, IsPropertySaveable.Yes)] public float RequiredAngle { get; set; } /// /// Legacy support. Use Afflictions. /// - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float Stun { get; private set; } - [Serialize(false, true, description: "Can damage only Humans."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] public bool OnlyHumans { get; private set; } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string ApplyForceOnLimbs { get @@ -211,54 +211,54 @@ namespace Barotrauma } } - [Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldStart { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldMiddle { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldEnd { get; private set; } - [Serialize(TransitionMode.Linear, true, description:""), Editable] + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable] public TransitionMode RootTransitionEasing { get; private set; } - [Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)] public float Torque { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool ApplyForcesOnlyOnce { get; private set; } - [Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float TargetImpulse { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetImpulseWorld { get; private set; } - [Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)] public float TargetForce { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetForceWorld { get; private set; } - [Serialize(1.0f, true, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float SubmarineImpactMultiplier { get; private set; } - [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SeverLimbsProbability { get; set; } // TODO: disabled because not synced - //[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + //[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] //public float StickChance { get; set; } public float StickChance => 0f; - [Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Priority { get; private set; } - [Serialize(false, true, description: ""), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: ""), Editable] public bool Blink { get; private set; } public IEnumerable StatusEffects @@ -268,11 +268,11 @@ namespace Barotrauma public string Name => "Attack"; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; - } = new Dictionary(); + } = new Dictionary(); //the indices of the limbs Force is applied on //(if none, force is applied only to the limb the attack is attached to) @@ -347,11 +347,12 @@ namespace Barotrauma Penetration = Penetration; } - public Attack(XElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName) + public Attack(ContentXElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName) { SourceItem = sourceItem; } - public Attack(XElement element, string parentDebugName) + + public Attack(ContentXElement element, string parentDebugName) { Deserialize(element); @@ -372,7 +373,7 @@ namespace Barotrauma InitProjSpecific(element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -395,7 +396,7 @@ namespace Barotrauma else { string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); + afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier]; if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found."); @@ -415,7 +416,7 @@ namespace Barotrauma } } } - partial void InitProjSpecific(XElement element = null); + partial void InitProjSpecific(ContentXElement element); public void ReloadAfflictions(XElement element) { @@ -424,13 +425,8 @@ namespace Barotrauma { AfflictionPrefab afflictionPrefab; Affliction affliction; - string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); - if (afflictionPrefab == null) - { - DebugConsole.ThrowError($"Couldn't find the affliction with the identifier {afflictionIdentifier} referenced in {element.Document.ParseContentPathFromUri()}"); - continue; - } + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier]; affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); //backwards compatibility diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 7850c8f59..3c0307d51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using FarseerPhysics.Dynamics; using Barotrauma.Extensions; +using System.Collections.Immutable; using Barotrauma.Abilities; #if SERVER using System.Text; @@ -26,7 +27,7 @@ namespace Barotrauma partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable { - public static List CharacterList = new List(); + public readonly static List CharacterList = new List(); partial void UpdateLimbLightSource(Limb limb); @@ -103,8 +104,8 @@ namespace Barotrauma public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; public bool IsEscorted { get; set; } - public readonly Dictionary Properties; - public Dictionary SerializableProperties + public readonly Dictionary Properties; + public Dictionary SerializableProperties { get { return Properties; } } @@ -116,7 +117,7 @@ namespace Barotrauma protected Key[] keys; - public HumanPrefab Prefab; + public HumanPrefab HumanPrefab; private CharacterTeamType teamID; public CharacterTeamType TeamID @@ -157,7 +158,9 @@ namespace Barotrauma return; } // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated) - SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, orderGiver: this, speak: false); + var order = new Order(OrderPrefab.Dismissal, Identifier.Empty, + manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); + SetOrder(order, speak: false); #if SERVER GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange }); @@ -222,7 +225,9 @@ namespace Barotrauma if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior { - SetOrder(Order.GetPrefab("fightintruders"), orderOption: null, priority: 3, orderGiver: this, speak: false); + var order = new Order(OrderPrefab.Prefabs["fightintruders"], Identifier.Empty, + manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); + SetOrder(order, speak: false); } } } @@ -270,11 +275,11 @@ namespace Barotrauma public float InvisibleTimer; - private readonly CharacterPrefab prefab; + public readonly CharacterPrefab Prefab; public readonly CharacterParams Params; - public string SpeciesName => Params?.SpeciesName ?? "null"; - public string Group => Params.Group; + public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); + public Identifier Group => Params.Group; public bool IsHumanoid => Params.Humanoid; public bool IsHusk => Params.Husk; @@ -318,22 +323,13 @@ namespace Barotrauma set; } - public string TraitorCurrentObjective = ""; - public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); - - /// - /// Can be used by status effects to check the character's gender - /// - public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male; - /// - /// Can be used by status effects to check the character's gender - /// - public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female; + public LocalizedString TraitorCurrentObjective = ""; + public bool IsHuman => SpeciesName == CharacterPrefab.HumanSpeciesName; private float attackCoolDown; - public List CurrentOrders => Info?.CurrentOrders; - public bool IsDismissed => !GetCurrentOrderWithTopPriority().HasValue; + public List CurrentOrders => Info?.CurrentOrders; + public bool IsDismissed => GetCurrentOrderWithTopPriority() == null; private readonly List statusEffects = new List(); @@ -380,13 +376,13 @@ namespace Barotrauma } } - public string VariantOf { get; private set; } + public Identifier VariantOf => Prefab.VariantOf; public string Name { get { - return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName; + return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName.Value; } } @@ -401,19 +397,19 @@ namespace Barotrauma } if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; } - var displayName = Params.DisplayName; - if (string.IsNullOrWhiteSpace(displayName)) + LocalizedString displayName = Params.DisplayName; + if (displayName.IsNullOrWhiteSpace()) { if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride)) { - displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true); + displayName = TextManager.Get($"Character.{SpeciesName}"); } else { - displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}", returnNull: true); + displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}"); } } - return string.IsNullOrWhiteSpace(displayName) ? Name : displayName; + return displayName.IsNullOrWhiteSpace() ? Name : displayName.Value; } } @@ -423,7 +419,7 @@ namespace Barotrauma get { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises) return Name; - return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName; + return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName.Value; } } @@ -441,7 +437,7 @@ namespace Barotrauma } } - public string ConfigPath => Params.File; + public string ConfigPath => Params.File.Path.Value; public float Mass { @@ -456,7 +452,7 @@ namespace Barotrauma public bool ResetInteract; //text displayed when the character is highlighted if custom interact is set - public string customInteractHUDText; + public LocalizedString CustomInteractHUDText { get; private set; } private Action onCustomInteract; public ConversationAction ActiveConversation; @@ -896,7 +892,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "Character.SimPosition:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error, - errorMsg.Replace("[name]", SpeciesName) + "\n" + Environment.StackTrace.CleanupStackTrace()); + errorMsg.Replace("[name]", SpeciesName.Value) + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } return Vector2.Zero; @@ -958,34 +954,42 @@ namespace Barotrauma { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { - speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); + speciesName = Path.GetFileNameWithoutExtension(speciesName); } + return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll); + } + public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + { var prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab == null) { DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace); return null; } + return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll); + } + public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + { Character newCharacter = null; - if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (prefab.Identifier != CharacterPrefab.HumanSpeciesName) { - var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); var ai = new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else if (hasAi) { - var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); var ai = new HumanAIController(aiCharacter); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else { - newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); } float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration; @@ -1012,8 +1016,8 @@ namespace Barotrauma void AddDamageReduction(string affliction, float amount, ActionType actionType = ActionType.Always) { newCharacter.statusEffects.Add(StatusEffect.Load( - new XElement("StatusEffect", new XAttribute("type", actionType), new XAttribute("target", "Character"), - new XElement("ReduceAffliction", new XAttribute("identifier", affliction), new XAttribute("amount", amount))), $"automatic damage reduction ({affliction})")); + new XElement("StatusEffect", new XAttribute("type", actionType), new XAttribute("target", "Character"), + new XElement("ReduceAffliction", new XAttribute("identifier", affliction), new XAttribute("amount", amount))).FromPackage(null), $"automatic damage reduction ({affliction})")); } #if SERVER @@ -1025,12 +1029,11 @@ namespace Barotrauma return newCharacter; } - protected Character(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) + protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) : base(null, id) { - VariantOf = prefab.VariantOf; this.Seed = seed; - this.prefab = prefab; + this.Prefab = prefab; MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); IsRemotePlayer = isRemotePlayer; @@ -1042,15 +1045,15 @@ namespace Barotrauma Properties = SerializableProperty.GetProperties(this); - Params = new CharacterParams(prefab.FilePath); + Params = new CharacterParams(prefab.ContentFile as CharacterFile); Info = characterInfo; - speciesName = VariantOf ?? speciesName; + Identifier speciesName = prefab.Identifier; - if (speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (VariantOf == CharacterPrefab.HumanSpeciesName || speciesName == CharacterPrefab.HumanSpeciesName) { - if (VariantOf != null) + if (!VariantOf.IsEmpty) { DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!"); } @@ -1069,19 +1072,14 @@ namespace Barotrauma keys[i] = new Key((InputType)i); } - var rootElement = prefab.XDocument.Root; - if (VariantOf != null) - { - rootElement = CharacterPrefab.FindBySpeciesName(VariantOf)?.XDocument?.Root; - } - var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; + var mainElement = prefab.ConfigElement; InitProjSpecific(mainElement); - List inventoryElements = new List(); + List inventoryElements = new List(); List inventoryCommonness = new List(); - List healthElements = new List(); + List healthElements = new List(); List healthCommonness = new List(); - foreach (XElement subElement in mainElement.Elements()) + foreach (var subElement in mainElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1100,13 +1098,13 @@ namespace Barotrauma } if (Params.VariantFile != null) { - XElement overrideElement = Params.VariantFile.Root; + var overrideElement = Params.VariantFile.Root.FromPackage(Params.MainElement.ContentPackage); // Only override if the override file contains matching elements if (overrideElement.GetChildElement("inventory") != null) { inventoryElements.Clear(); inventoryCommonness.Clear(); - foreach (XElement subElement in overrideElement.GetChildElements("inventory")) + foreach (var subElement in overrideElement.GetChildElements("inventory")) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1121,7 +1119,7 @@ namespace Barotrauma { healthElements.Clear(); healthCommonness.Clear(); - foreach (XElement subElement in overrideElement.GetChildElements("health")) + foreach (var subElement in overrideElement.GetChildElements("health")) { healthElements.Add(subElement); healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f)); @@ -1157,13 +1155,13 @@ namespace Barotrauma var matchingAffliction = AfflictionPrefab.List .Where(p => p is AfflictionPrefabHusk) .Select(p => p as AfflictionPrefabHusk) - .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase))); - string nonHuskedSpeciesName = string.Empty; + .FirstOrDefault(p => p.TargetSpecies.Any(t => t == AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p))); + Identifier nonHuskedSpeciesName = Identifier.Empty; if (matchingAffliction == null) { DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!"); // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. - nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler"; + nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier(); speciesName = nonHuskedSpeciesName; } else @@ -1172,7 +1170,7 @@ namespace Barotrauma } if (ragdollParams == null && prefab.VariantOf == null) { - string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; + Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name) : RagdollParams.GetDefaultRagdollParams(name) as RagdollParams; } if (Params.HasInfo && info == null) @@ -1214,14 +1212,20 @@ namespace Barotrauma } ApplyStatusEffects(ActionType.OnSpawn, 1.0f); } - partial void InitProjSpecific(XElement mainElement); + partial void InitProjSpecific(ContentXElement mainElement); public void ReloadHead(int? headId = null, int hairIndex = -1, int beardIndex = -1, int moustacheIndex = -1, int faceAttachmentIndex = -1) { if (Info == null) { return; } var head = AnimController.GetLimb(LimbType.Head); if (head == null) { return; } - Info.RecreateHead(headId ?? Info.HeadSpriteId, Info.Race, Info.Gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + HashSet tags = Info.Head.Preset.TagSet.ToHashSet(); + if (headId.HasValue) + { + tags.RemoveWhere(t => t.StartsWith("variant")); + tags.Add($"variant{headId.Value}".ToIdentifier()); + } + Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); #if CLIENT head.RecreateSprites(); #endif @@ -1239,19 +1243,19 @@ namespace Barotrauma head.OtherWearables.Clear(); //if the element has not been set at this point, the character has no hair and the index should be zero (= no hair) - if (info.FaceAttachment == null) { info.FaceAttachmentIndex = 0; } - Info.FaceAttachment?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.FaceAttachment))); - if (info.BeardElement == null) { info.BeardIndex = 0; } - Info.BeardElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Beard))); - if (info.MoustacheElement == null) { info.MoustacheIndex = 0; } - Info.MoustacheElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Moustache))); - if (info.HairElement == null) { info.HairIndex = 0; } - Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair))); + if (info.Head.FaceAttachment == null) { info.Head.FaceAttachmentIndex = 0; } + Info.Head.FaceAttachment?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.FaceAttachment))); + if (info.Head.BeardElement == null) { info.Head.BeardIndex = 0; } + Info.Head.BeardElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Beard))); + if (info.Head.MoustacheElement == null) { info.Head.MoustacheIndex = 0; } + Info.Head.MoustacheElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Moustache))); + if (info.Head.HairElement == null) { info.Head.HairIndex = 0; } + Info.Head.HairElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair))); #if CLIENT - if (info.Head?.HairWithHatElement != null) + if (info.Head?.HairWithHatElement?.GetChildElement("sprite") != null) { - head.HairWithHatSprite = new WearableSprite(info.Head?.HairWithHatElement.Element("sprite"), WearableType.Hair); + head.HairWithHatSprite = new WearableSprite(info.Head.HairWithHatElement.GetChildElement("sprite"), WearableType.Hair); } head.EnableHuskSprite = Params.Husk; head.LoadHerpesSprite(); @@ -1391,9 +1395,9 @@ namespace Barotrauma public override string ToString() { #if DEBUG - return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName; + return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName.Value; #else - return SpeciesName; + return SpeciesName.Value; #endif } @@ -1416,12 +1420,15 @@ namespace Barotrauma } if (createNetworkEvent && (GameMain.NetworkMember?.IsServer ?? false)) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, item.SerializableProperties["tags"] }); + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, item.SerializableProperties[nameof(item.Tags).ToIdentifier()] }); } } } - public float GetSkillLevel(string skillIdentifier) + public float GetSkillLevel(string skillIdentifier) => + GetSkillLevel(skillIdentifier.ToIdentifier()); + + public float GetSkillLevel(Identifier skillIdentifier) { if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); @@ -2094,6 +2101,9 @@ namespace Barotrauma } public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) + => HasEquippedItem(tagOrIdentifier.ToIdentifier(), allowBroken, slotType); + + public bool HasEquippedItem(Identifier tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) { if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) @@ -2164,8 +2174,8 @@ namespace Barotrauma /// The method is run in steps for performance reasons. So you'll have to provide the reference to the itemIndex. /// Returns false while running and true when done. /// - public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable identifiers = null, bool ignoreBroken = true, - IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, + public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable identifiers = null, bool ignoreBroken = true, + IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, Func customPredicate = null, Func customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null) { if (itemIndex == 0) @@ -2194,13 +2204,13 @@ namespace Barotrauma if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } } if (IsItemTakenBySomeoneElse(item)) { continue; } - float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; - if (itemPriority <= 0) { continue; } Entity rootInventoryOwner = item.GetRootInventoryOwner(); if (rootInventoryOwner is Item ownerItem) { if (!ownerItem.IsInteractable(this)) { continue; } } + float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; + if (itemPriority <= 0) { continue; } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition; float yDist = Math.Abs(refPos.Y - itemPos.Y); @@ -2316,7 +2326,7 @@ namespace Barotrauma } bool insideTrigger = item.IsInsideTrigger(upperBodyPosition) || item.IsInsideTrigger(lowerBodyPosition); - if (item.Prefab.Triggers.Count > 0 && !insideTrigger && item.Prefab.RequireBodyInsideTrigger) { return false; } + if (item.Prefab.Triggers.Length > 0 && !insideTrigger && item.Prefab.RequireBodyInsideTrigger) { return false; } Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height); @@ -2362,7 +2372,10 @@ namespace Barotrauma itemPosition -= Submarine.SimPosition; } var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); - if (body != null && body.UserData as Item != item && Submarine.LastPickedFixture?.UserData as Item != item) { return false; } + if (body != null && body.UserData as Item != item && (body.UserData as ItemComponent)?.Item != item && Submarine.LastPickedFixture?.UserData as Item != item) + { + return false; + } } return true; @@ -2373,10 +2386,10 @@ namespace Barotrauma /// /// Action invoked when another character interacts with this one. T1 = this character, T2 = the interacting character /// Displayed on the character when highlighted. - public void SetCustomInteract(Action onCustomInteract, string hudText) + public void SetCustomInteract(Action onCustomInteract, LocalizedString hudText) { this.onCustomInteract = onCustomInteract; - customInteractHUDText = hudText; + CustomInteractHUDText = hudText; } private void TransformCursorPos() @@ -2449,7 +2462,7 @@ namespace Barotrauma { FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } - float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); + float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes @@ -2627,7 +2640,7 @@ namespace Barotrauma c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) { - Spawner?.AddToRemoveQueue(c); + Spawner?.AddEntityToRemoveQueue(c); } } else if (closestPlayerDist < c.Params.DisableDistance * 0.9f) @@ -2653,7 +2666,7 @@ namespace Barotrauma c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) { - Entity.Spawner?.AddToRemoveQueue(c); + Entity.Spawner?.AddEntityToRemoveQueue(c); } } else if (distSqr < MathUtils.Pow2(c.Params.DisableDistance * 0.9f)) @@ -2892,7 +2905,7 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam); - partial void SetOrderProjSpecific(Order order, string orderOption, int priority); + partial void SetOrderProjSpecific(Order order); public void AddAttacker(Character character, float damage) @@ -3051,7 +3064,7 @@ namespace Barotrauma if (Submarine != null && !ignoreThresholds) { subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); - if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; } + if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; } } if (SelectedBy != null) @@ -3064,14 +3077,14 @@ namespace Barotrauma if (distToClosestPlayer > Params.DisableDistance) { //despawn in 1 minute if very far from all human players - despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f); + despawnTimer = Math.Max(despawnTimer, GameSettings.CurrentConfig.CorpseDespawnDelay - 60.0f); } float despawnPriority = 1.0f; - if (subCorpseCount > GameMain.Config.CorpsesPerSubDespawnThreshold) + if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { //despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast) - despawnPriority += (subCorpseCount - GameMain.Config.CorpsesPerSubDespawnThreshold) / (float)GameMain.Config.CorpsesPerSubDespawnThreshold; + despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold; } if (AIController is EnemyAIController) { @@ -3080,20 +3093,20 @@ namespace Barotrauma } despawnTimer += deltaTime * despawnPriority; - if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; } + if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; } if (IsHuman) { var containerPrefab = ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ?? - (MapEntityPrefab.Find(null, identifier: "metalcrate") as ItemPrefab); + (MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab); if (containerPrefab == null) { DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red); } else { - Spawner?.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned); + Spawner?.AddItemToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned); } void onItemContainerSpawned(Item item) @@ -3102,7 +3115,7 @@ namespace Barotrauma item.UpdateTransform(); item.AddTag("name:" + Name); - if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); } + if (info?.Job != null) { item.AddTag($"job:{info.Job.Name}"); } var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } @@ -3117,12 +3130,12 @@ namespace Barotrauma } } - Spawner.AddToRemoveQueue(this); + Spawner.AddEntityToRemoveQueue(this); } public void DespawnNow(bool createNetworkEvents = true) { - despawnTimer = GameMain.Config.CorpseDespawnDelay; + despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); Spawner.Update(createNetworkEvents); } @@ -3133,7 +3146,7 @@ namespace Barotrauma List list = new List(CharacterList); foreach (Character character in list) { - if (character.prefab == prefab) + if (character.Prefab == prefab) { character.Remove(); } @@ -3186,17 +3199,14 @@ namespace Barotrauma } /// Force an order to be set for the character, bypassing hearing checks - public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true, bool force = false) + public void SetOrder(Order order, bool speak = true, bool force = false) { + var orderGiver = order?.OrderGiver; //set the character order only if the character is close enough to hear the message if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; } if (order != null) { - if (order.OrderGiver != orderGiver) - { - order.OrderGiver = orderGiver; - } if (order.AutoDismiss) { switch (order.Category) @@ -3211,29 +3221,29 @@ namespace Barotrauma if (!HumanAIController.IsActive(character)) { continue; } foreach (var currentOrder in character.CurrentOrders) { - if (currentOrder.Order == null) { continue; } - if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } - if (currentOrder.Order.Identifier != order.Identifier) { continue; } - if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } - if (!currentOrder.Order.AutoDismiss) { continue; } - character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force); + if (currentOrder == null) { continue; } + if (currentOrder.Category != OrderCategory.Operate) { continue; } + if (currentOrder.Identifier != order.Identifier) { continue; } + if (currentOrder.TargetEntity != order.TargetEntity) { continue; } + if (!currentOrder.AutoDismiss) { continue; } + character.SetOrder(currentOrder.GetDismissal(), speak: speak, force: force); break; } } break; case OrderCategory.Movement: // If there character has another movement order, dismiss that order - OrderInfo? orderToReplace = null; + Order orderToReplace = null; foreach (var currentOrder in CurrentOrders) { - if (currentOrder.Order == null) { continue; } - if (currentOrder.Order.Category != OrderCategory.Movement) { continue; } + if (currentOrder == null) { continue; } + if (currentOrder.Category != OrderCategory.Movement) { continue; } orderToReplace = currentOrder; break; } - if (orderToReplace.HasValue && orderToReplace.Value.Order.AutoDismiss) + if (orderToReplace is { AutoDismiss: true }) { - SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force); + SetOrder(orderToReplace.GetDismissal(), speak: speak, force: force); } break; } @@ -3241,45 +3251,37 @@ namespace Barotrauma } // Prevent adding duplicate orders - RemoveDuplicateOrders(order, orderOption); + bool wasDuplicate = RemoveDuplicateOrders(order); + AddCurrentOrder(order); - OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority); - AddCurrentOrder(newOrderInfo); - - if (orderGiver != null) + if (orderGiver != null && order.Identifier != "dismissed" && !wasDuplicate) { var abilityOrderedCharacter = new AbilityOrderedCharacter(this); orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter); - if (orderGiver.LastOrderedCharacter != this) + if (order.OrderGiver.LastOrderedCharacter != this) { - orderGiver.SecondLastOrderedCharacter = orderGiver.LastOrderedCharacter; - orderGiver.LastOrderedCharacter = this; + order.OrderGiver.SecondLastOrderedCharacter = order.OrderGiver.LastOrderedCharacter; + order.OrderGiver.LastOrderedCharacter = this; } } if (AIController is HumanAIController humanAI) { - humanAI.SetOrder(order, orderOption, priority, orderGiver, speak); + humanAI.SetOrder(order, speak); } - SetOrderProjSpecific(order, orderOption, priority); + SetOrderProjSpecific(order); } - /// Force an order to be set for the character, bypassing hearing checks - public void SetOrder(OrderInfo orderInfo, Character orderGiver, bool speak = true, bool force = false) + private void AddCurrentOrder(Order newOrder) { - SetOrder(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, orderGiver, speak: speak, force: force); - } - - private void AddCurrentOrder(OrderInfo newOrder) - { - if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed") + if (newOrder == null || newOrder.Identifier == "dismissed") { - if (!string.IsNullOrEmpty(newOrder.OrderOption)) + if (newOrder.Option != Identifier.Empty) { - if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.OrderOption))) + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.Option))) { - var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.OrderOption)); + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.Option)); int dismissedOrderPriority = dismissedOrderInfo.ManualPriority; CurrentOrders.Remove(dismissedOrderInfo); for (int i = 0; i < CurrentOrders.Count; i++) @@ -3287,7 +3289,7 @@ namespace Barotrauma var orderInfo = CurrentOrders[i]; if (orderInfo.ManualPriority < dismissedOrderPriority) { - CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority + 1); } } } @@ -3304,7 +3306,7 @@ namespace Barotrauma var orderInfo = CurrentOrders[i]; if (orderInfo.ManualPriority <= newOrder.ManualPriority) { - CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority - 1); + CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority - 1); } } CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); @@ -3314,56 +3316,60 @@ namespace Barotrauma } } - private void RemoveDuplicateOrders(Order order, string option) + private bool RemoveDuplicateOrders(Order order) { + bool removed = false; int? priorityOfRemoved = null; for (int i = CurrentOrders.Count - 1; i >= 0; i--) { var orderInfo = CurrentOrders[i]; - if (order?.Identifier == orderInfo.Order?.Identifier) + if (order.Identifier == orderInfo.Identifier) { priorityOfRemoved = orderInfo.ManualPriority; CurrentOrders.RemoveAt(i); + removed = true; break; } } - if (!priorityOfRemoved.HasValue) { return; } + if (!priorityOfRemoved.HasValue) { return removed; } for (int i = 0; i < CurrentOrders.Count; i++) { var orderInfo = CurrentOrders[i]; if (orderInfo.ManualPriority < priorityOfRemoved.Value) { - CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority + 1); } } CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); // Sort the current orders so the one with the highest priority comes first CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + + return removed; } - public OrderInfo? GetCurrentOrderWithTopPriority() + public Order GetCurrentOrderWithTopPriority() { return GetCurrentOrder(orderInfo => { - if (orderInfo.Order == null) { return false; } - if (orderInfo.Order.Identifier == "dismissed") { return false; } + if (orderInfo == null) { return false; } + if (orderInfo.Identifier == "dismissed") { return false; } if (orderInfo.ManualPriority < 1) { return false; } return true; }); } - public OrderInfo? GetCurrentOrder(Order order, string option) + public Order GetCurrentOrder(Order order) { return GetCurrentOrder(orderInfo => { - return orderInfo.MatchesOrder(order, option); + return orderInfo.MatchesOrder(order); }); } - private OrderInfo? GetCurrentOrder(Func predicate) + private Order GetCurrentOrder(Func predicate) { if (CurrentOrders != null && CurrentOrders.Any(predicate)) { @@ -3378,17 +3384,22 @@ namespace Barotrauma private readonly List aiChatMessageQueue = new List(); //key = identifier, value = time the message was sent - private readonly Dictionary prevAiChatMessages = new Dictionary(); + private readonly Dictionary prevAiChatMessages = new Dictionary(); - public void DisableLine(string identifier) + public void DisableLine(Identifier identifier) { - if (!string.IsNullOrEmpty(identifier)) + if (identifier != Identifier.Empty) { prevAiChatMessages[identifier] = (float)Timing.TotalTime; } } - public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f) + public void DisableLine(string identifier) + { + DisableLine(identifier.ToIdentifier()); + } + + public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, Identifier identifier = default, float minDurationBetweenSimilar = 0.0f) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (string.IsNullOrEmpty(message)) { return; } @@ -3402,7 +3413,7 @@ namespace Barotrauma } //already sent a similar message a moment ago - if (!string.IsNullOrEmpty(identifier) && minDurationBetweenSimilar > 0.0f && + if (identifier != Identifier.Empty && minDurationBetweenSimilar > 0.0f && (aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier))) { return; @@ -3448,7 +3459,7 @@ namespace Barotrauma { sent.SendTime = Timing.TotalTime; aiChatMessageQueue.Remove(sent); - if (!string.IsNullOrEmpty(sent.Identifier)) + if (sent.Identifier != Identifier.Empty) { prevAiChatMessages[sent.Identifier] = (float)sent.SendTime; } @@ -3456,15 +3467,15 @@ namespace Barotrauma if (prevAiChatMessages.Count > 100) { - List toRemove = new List(); - foreach (KeyValuePair prevMessage in prevAiChatMessages) + HashSet toRemove = new HashSet(); + foreach (KeyValuePair prevMessage in prevAiChatMessages) { if (prevMessage.Value < Timing.TotalTime - 60.0f) { toRemove.Add(prevMessage.Key); } } - foreach (string identifier in toRemove) + foreach (Identifier identifier in toRemove) { prevAiChatMessages.Remove(identifier); } @@ -3497,7 +3508,7 @@ namespace Barotrauma { string errorMsg = "Tried to apply an attack to a removed character ([name]).\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); - GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); return new AttackResult(); } @@ -3674,17 +3685,17 @@ namespace Barotrauma CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill); if (!IsOnPlayerTeam) { return; } - if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } - GameMain.Config.KilledCreatures.Add(target.SpeciesName); + if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; } + CreatureMetrics.Instance.Killed.Add(target.SpeciesName); AddEncounter(target); } public void AddEncounter(Character other) { if (!IsOnPlayerTeam) { return; } - if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(other.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } - GameMain.Config.EncounteredCreatures.Add(other.SpeciesName); - GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName); + if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; } + CreatureMetrics.Instance.Encountered.Add(other.SpeciesName); + CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName); } public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) @@ -3789,14 +3800,14 @@ namespace Barotrauma if (healthChange < 0.0f) { float attackerSkillLevel = attacker.GetSkillLevel("weapons"); - attacker.Info?.IncreaseSkillLevel("weapons", + attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(), -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f)); } } else if (healthChange > 0.0f) { float attackerSkillLevel = attacker.GetSkillLevel("medical"); - attacker.Info?.IncreaseSkillLevel("medical", + attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(), healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f)); } } @@ -3988,7 +3999,7 @@ namespace Barotrauma if (GameAnalyticsManager.SendUserStatistics) { string causeOfDeathStr = causeOfDeathAffliction == null ? - causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Replace(" ", ""); + causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", ""); string characterType = GetCharacterType(this); GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + causeOfDeathStr); @@ -4157,7 +4168,7 @@ namespace Barotrauma { foreach (Item item in Inventory.AllItems) { - Spawner?.AddToRemoveQueue(item); + Spawner?.AddItemToRemoveQueue(item); } } @@ -4219,14 +4230,14 @@ namespace Barotrauma SaveInventory(Inventory, Info?.InventoryData); } - public void SpawnInventoryItems(Inventory inventory, XElement itemData) + public void SpawnInventoryItems(Inventory inventory, ContentXElement itemData) { SpawnInventoryItemsRecursive(inventory, itemData, new List()); } - private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element, List extraDuffelBags) + private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List extraDuffelBags) { - foreach (XElement itemElement in element.Elements()) + foreach (var itemElement in element.Elements()) { var newItem = Item.Load(itemElement, inventory.Owner.Submarine, createNetworkEvent: true, idRemap: IdRemap.DiscardId); if (newItem == null) { continue; } @@ -4253,7 +4264,7 @@ namespace Barotrauma if (slotIndices.Contains(i)) { var existingItem = inventory.GetItemAt(i); - if (existingItem != null && existingItem != newItem && (existingItem.prefab != newItem.prefab || existingItem.Prefab.MaxStackSize == 1)) + if (existingItem != null && existingItem != newItem && (((MapEntity)existingItem).Prefab != ((MapEntity)newItem).Prefab || existingItem.Prefab.MaxStackSize == 1)) { DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\""); existingItem.Drop(null, createNetworkEvent: false); @@ -4306,7 +4317,7 @@ namespace Barotrauma { // In case the inventory capacity is smaller than it was when saving: // 1) Spawn a new duffel bag if none yet spawned or if the existing ones aren't enough - if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.Find(null, "duffelbag") is ItemPrefab duffelBagPrefab) + if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.FindByIdentifier("duffelbag".ToIdentifier()) is ItemPrefab duffelBagPrefab) { var hull = Hull.FindHull(WorldPosition, guess: CurrentHull); var mainSub = Submarine.MainSubs.FirstOrDefault(s => s.TeamID == TeamID); @@ -4344,7 +4355,7 @@ namespace Barotrauma int itemContainerIndex = 0; var itemContainers = newItem.GetComponents().ToList(); - foreach (XElement childInvElement in itemElement.Elements()) + foreach (var childInvElement in itemElement.Elements()) { if (itemContainerIndex >= itemContainers.Count) break; if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -4488,29 +4499,29 @@ namespace Barotrauma public void LoadTalents() { - List toBeRemoved = null; - foreach (string talent in info.UnlockedTalents) + List toBeRemoved = null; + foreach (Identifier talent in info.UnlockedTalents) { if (!GiveTalent(talent, addingFirstTime: false)) { DebugConsole.AddWarning(Name + " had talent that did not exist! Removing talent from CharacterInfo."); - toBeRemoved ??= new List(); + toBeRemoved ??= new List(); toBeRemoved.Add(talent); } } if (toBeRemoved != null) { - foreach (string removeTalent in toBeRemoved) + foreach (Identifier removeTalent in toBeRemoved) { Info.UnlockedTalents.Remove(removeTalent); } } } - public bool GiveTalent(string talentIdentifier, bool addingFirstTime = true) + public bool GiveTalent(Identifier talentIdentifier, bool addingFirstTime = true) { - TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talentIdentifier, StringComparison.OrdinalIgnoreCase)); + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier == talentIdentifier); if (talentPrefab == null) { DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); @@ -4521,7 +4532,7 @@ namespace Barotrauma public bool GiveTalent(UInt32 talentIdentifier, bool addingFirstTime = true) { - TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UIntIdentifier == talentIdentifier); + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UintIdentifier == talentIdentifier); if (talentPrefab == null) { DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); @@ -4546,20 +4557,20 @@ namespace Barotrauma if (addingFirstTime) { OnTalentGiven(talentPrefab); - GameAnalyticsManager.AddDesignEvent("TalentUnlocked:" + (info.Job?.Prefab.Identifier ?? "None") + ":" + talentPrefab.Identifier, + GameAnalyticsManager.AddDesignEvent("TalentUnlocked:" + (info.Job?.Prefab.Identifier ?? "None".ToIdentifier()) + ":" + talentPrefab.Identifier, GameMain.GameSession?.Campaign?.TotalPlayTime ?? 0.0); } return true; } - public bool HasTalent(string identifier) + public bool HasTalent(Identifier identifier) { return info.UnlockedTalents.Contains(identifier); } public bool HasUnlockedAllTalents() { - if (TalentTree.JobTalentTrees.TryGetValue(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) { foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) { @@ -4605,7 +4616,7 @@ namespace Barotrauma } } - public bool HasRecipeForItem(string recipeIdentifier) + public bool HasRecipeForItem(Identifier recipeIdentifier) { return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); } @@ -4707,11 +4718,11 @@ namespace Barotrauma statValues.Add(statType, value); } } - - public static StatTypes GetSkillStatType(string skillIdentifier) + + private static StatTypes GetSkillStatType(Identifier skillIdentifier) { // Using this method to translate between skill identifiers and stat types. Feel free to replace it if there's a better way - switch (skillIdentifier) + switch (skillIdentifier.Value.ToLowerInvariant()) { case "electrical": return StatTypes.ElectricalSkillBonus; @@ -4745,14 +4756,14 @@ namespace Barotrauma return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); } - private readonly Dictionary abilityResistances = new Dictionary(); - + private readonly Dictionary abilityResistances = new Dictionary(); + public float GetAbilityResistance(AfflictionPrefab affliction) { return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f; } - public void ChangeAbilityResistance(string resistanceId, float value) + public void ChangeAbilityResistance(Identifier resistanceId, float value) { if (abilityResistances.ContainsKey(resistanceId)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 85d189f96..e008a0432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -11,55 +11,91 @@ using Barotrauma.Abilities; namespace Barotrauma { - public enum Gender { None, Male, Female }; - public enum Race { None, White, Black, Brown, Asian }; - + class CharacterInfoPrefab + { + public readonly ImmutableArray Heads; + public readonly ImmutableDictionary> VarTags; + public readonly Identifier MenuCategoryVar; + public readonly Identifier Pronouns; + + public CharacterInfoPrefab(ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement) + { + Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray(); + VarTags = varsElement.Elements() + .Select(e => + (e.GetAttributeIdentifier("var", ""), + e.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet())) + .ToImmutableDictionary(); + MenuCategoryVar = menuCategoryElement.GetAttributeIdentifier("var", Identifier.Empty); + Pronouns = pronounsElement.GetAttributeIdentifier("vars", Identifier.Empty); + } + public string ReplaceVars(string str, CharacterInfo.HeadPreset headPreset) + { + return ReplaceVars(str, headPreset.TagSet); + } + + public string ReplaceVars(string str, ImmutableHashSet tagSet) + { + foreach (var key in VarTags.Keys) + { + str = str.Replace($"[{key}]", tagSet.FirstOrDefault(t => VarTags[key].Contains(t)).Value, StringComparison.OrdinalIgnoreCase); + } + return str; + } + } + partial class CharacterInfo { public class HeadInfo { - private int _headSpriteId; - public int HeadSpriteId + public readonly CharacterInfo CharacterInfo; + public readonly HeadPreset Preset; + + private int hairIndex; + + public int HairIndex { - get { return _headSpriteId; } + get => hairIndex; set { - _headSpriteId = Math.Max(Math.Clamp(value, (int)headSpriteRange.X, (int)headSpriteRange.Y), 1); - GetSpriteSheetIndex(); + hairIndex = value; + if (CharacterInfo.Hairs is null) + { + HairWithHatIndex = value; + return; + } + HairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", hairIndex) ?? -1; + if (HairWithHatIndex < 0 || HairWithHatIndex >= CharacterInfo.Hairs.Count) + { + HairWithHatIndex = hairIndex; + } } } - public Vector2? SheetIndex { get; private set; } - public Vector2 headSpriteRange; - public Gender gender; - public Race race; + public int HairWithHatIndex { get; private set; } + public int BeardIndex; + public int MoustacheIndex; + public int FaceAttachmentIndex; public Color HairColor; public Color FacialHairColor; public Color SkinColor; - public int HairIndex { get; set; } = -1; - public int BeardIndex { get; set; } = -1; - public int MoustacheIndex { get; set; } = -1; - public int FaceAttachmentIndex { get; set; } = -1; + public Vector2 SheetIndex => Preset.SheetIndex; - public XElement HairElement { get; set; } - public XElement HairWithHatElement { get; set; } - public XElement BeardElement { get; set; } - public XElement MoustacheElement { get; set; } - public XElement FaceAttachment { get; set; } - - public HeadInfo() { } + public ContentXElement HairElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairIndex); + public ContentXElement HairWithHatElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairWithHatIndex); + public ContentXElement BeardElement => CharacterInfo.Beards?.ElementAtOrDefault(BeardIndex); + public ContentXElement MoustacheElement => CharacterInfo.Moustaches?.ElementAtOrDefault(MoustacheIndex); + public ContentXElement FaceAttachment => CharacterInfo.FaceAttachments?.ElementAtOrDefault(FaceAttachmentIndex); - public HeadInfo(int headId, Gender gender, Race race, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0) + public HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0) { - _headSpriteId = Math.Max(headId, 1); - this.gender = gender; - this.race = race; + CharacterInfo = characterInfo; + Preset = headPreset; HairIndex = hairIndex; BeardIndex = beardIndex; MoustacheIndex = moustacheIndex; FaceAttachmentIndex = faceAttachmentIndex; - GetSpriteSheetIndex(); } public void ResetAttachmentIndices() @@ -69,21 +105,6 @@ namespace Barotrauma MoustacheIndex = -1; FaceAttachmentIndex = -1; } - - public void GetSpriteSheetIndex() - { - if (heads != null && heads.Any()) - { - var matchingHead = heads.Keys.FirstOrDefault(h => h.ID == HeadSpriteId && IsMatchingGender(h.Gender, gender) && IsMatchingRace(h.Race, race)); - if (matchingHead != null) - { - if (heads.TryGetValue(matchingHead, out Vector2 index)) - { - SheetIndex = index; - } - } - } - } } private HeadInfo head; @@ -95,50 +116,38 @@ namespace Barotrauma if (head != value && value != null) { head = value; - if (!IsValidRace(head.race)) - { - head.race = GetRandomRace(Rand.RandSync.Unsynced); - } - CalculateHeadSpriteRange(); - Head.HeadSpriteId = value.HeadSpriteId; - RefreshHeadSprites(); + HeadSprite = null; + AttachmentSprites = null; } } } - public Dictionary Heads - { - get - { - if (heads == null) - { - LoadHeadPresets(); - } - return heads; - } - } - - private static Dictionary heads; + public CharacterInfoPrefab Prefab => CharacterPrefab.Prefabs[SpeciesName].CharacterInfoPrefab; public class HeadPreset : ISerializableEntity { - [Serialize(Race.None, false)] - public Race Race { get; private set; } + private readonly CharacterInfoPrefab characterInfoPrefab; + public Identifier MenuCategory => TagSet.First(t => characterInfoPrefab.VarTags[characterInfoPrefab.MenuCategoryVar].Contains(t)); - [Serialize(Gender.None, false)] - public Gender Gender { get; private set; } - [Serialize(0, false)] - public int ID { get; private set; } + public ImmutableHashSet TagSet { get; private set; } - [Serialize("0,0", false)] + [Serialize("", IsPropertySaveable.No)] + public string Tags + { + get { return string.Join(",", TagSet); } + private set { TagSet = value.Split(",").Select(s => s.ToIdentifier()).ToImmutableHashSet(); } + } + + [Serialize("0,0", IsPropertySaveable.No)] public Vector2 SheetIndex { get; private set; } - public string Name => $"Head Preset {Race} {Gender} {ID}"; + public string Name => $"Head Preset {Tags}"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - public HeadPreset(XElement element) + public HeadPreset(CharacterInfoPrefab charInfoPrefab, XElement element) { + characterInfoPrefab = charInfoPrefab; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } } @@ -172,37 +181,15 @@ namespace Barotrauma if (Character.Inventory != null) { + //Disguise as the ID card name if it's equipped var idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card); - if (idCard == null) { return disguiseName; } - - //Disguise as the ID card name if it's equipped - string[] readTags = idCard.Tags.Split(','); - foreach (string tag in readTags) - { - string[] s = tag.Split(':'); - if (s[0] == "name") - { - return s[1]; - } - } + return idCard?.GetComponent()?.OwnerName ?? disguiseName; } return disguiseName; } } - private string _speciesName; - public string SpeciesName - { - get - { - if (_speciesName == null) - { - _speciesName = CharacterConfigElement.GetAttributeString("speciesname", string.Empty).ToLowerInvariant(); - } - return _speciesName; - } - set { _speciesName = value; } - } + public Identifier SpeciesName { get; } /// /// Note: Can be null. @@ -215,14 +202,14 @@ namespace Barotrauma public int ExperiencePoints { get; private set; } - public HashSet UnlockedTalents { get; private set; } = new HashSet(); + public HashSet UnlockedTalents { get; private set; } = new HashSet(); /// /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection /// - public IEnumerable GetUnlockedTalentsInTree() + public IEnumerable GetUnlockedTalentsInTree() { - if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } + if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } return UnlockedTalents.Where(t => talentTree.TalentIsInTree(t)); } @@ -230,14 +217,21 @@ namespace Barotrauma /// /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them /// - public IEnumerable GetEndocrineTalents() + public IEnumerable GetEndocrineTalents() { - if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } + if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t)); } - public int AdditionalTalentPoints { get; set; } + public const int MaxAdditionalTalentPoints = 100; + + private int additionalTalentPoints; + public int AdditionalTalentPoints + { + get { return additionalTalentPoints; } + set { additionalTalentPoints = MathHelper.Clamp(value, 0, MaxAdditionalTalentPoints); } + } private Sprite _headSprite; public Sprite HeadSprite @@ -305,7 +299,7 @@ namespace Barotrauma { if (handleBuff) { - Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier.Equals("disguised", StringComparison.OrdinalIgnoreCase)).Instantiate(100f)); + Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); } idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); @@ -325,7 +319,7 @@ namespace Barotrauma if (handleBuff) { - Character.CharacterHealth.ReduceAffliction(Character.AnimController.GetLimb(LimbType.Head), "disguised", 100f); + Character.CharacterHealth.ReduceAfflictionOnLimb(Character.AnimController.GetLimb(LimbType.Head), "disguised".ToIdentifier(), 100f); } } @@ -350,7 +344,7 @@ namespace Barotrauma } } - public XElement CharacterConfigElement { get; set; } + public ContentXElement CharacterConfigElement { get; set; } public readonly string ragdollFileName = string.Empty; @@ -362,10 +356,11 @@ namespace Barotrauma public CharacterTeamType TeamID; - private NPCPersonalityTrait personalityTrait; + public NPCPersonalityTrait PersonalityTrait { get; private set; } public const int MaxCurrentOrders = 3; public static int HighestManualOrderPriority => MaxCurrentOrders; + public int GetManualOrderPriority(Order order) { if (order != null && order.AssignmentPriority < 100 && CurrentOrders.Any()) @@ -373,7 +368,7 @@ namespace Barotrauma int orderPriority = HighestManualOrderPriority; for (int i = 0; i < CurrentOrders.Count; i++) { - if (CurrentOrders[i].Order is Order currentOrder && order.AssignmentPriority >= currentOrder.AssignmentPriority) + if (order.AssignmentPriority >= CurrentOrders[i].AssignmentPriority) { break; } @@ -390,141 +385,19 @@ namespace Barotrauma } } - public List CurrentOrders { get; } = new List(); + public List CurrentOrders { get; } = new List(); //unique ID given to character infos in MP //used by clients to identify which infos are the same to prevent duplicate characters in round summary public ushort ID; - public List SpriteTags + public List SpriteTags { get; private set; } - public NPCPersonalityTrait PersonalityTrait - { - get { return personalityTrait; } - } - - /// - /// Setting the value with this property also resets the head attachments. Use Head.headSpriteId if you don't want that. - /// - public int HeadSpriteId - { - get { return Head.HeadSpriteId; } - set - { - Head.HeadSpriteId = value; - ResetHeadAttachments(); - RefreshHeadSprites(); - } - } - - public readonly bool HasGenders; - public readonly bool HasRaces; - - public Gender Gender - { - get { return Head.gender; } - set - { - Gender previousValue = Head.gender; - Head.gender = value; - if (!IsValidGender(Head.gender)) - { - Head.gender = GetDefaultGender(); - } - if (Head.gender != previousValue) - { - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - RefreshHeadSprites(); - } - } - } - - public Race Race - { - get { return Head.race; } - set - { - Race previousValue = Head.race; - Head.race = value; - if (!IsValidRace(Head.race)) - { - Head.race = GetDefaultRace(); - } - if (Head.race != previousValue) - { - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - RefreshHeadSprites(); - } - } - } - - private bool IsValidRace(Race race) => HasRaces ? race != Race.None : race == Race.None; - - private bool IsValidGender(Gender gender) => HasGenders ? gender != Gender.None : gender == Gender.None; - - private Gender GetDefaultGender() => HasGenders ? Gender.Male : Gender.None; - - private Race GetDefaultRace() => HasRaces ? Race.White : Race.None; - - public int HairIndex - { - get => Head.HairIndex; - set => Head.HairIndex = value; - } - - public int BeardIndex - { - get => Head.BeardIndex; - set => Head.BeardIndex = value; - } - - public int MoustacheIndex - { - get => Head.MoustacheIndex; - set => Head.MoustacheIndex = value; - } - - public int FaceAttachmentIndex - { - get => Head.FaceAttachmentIndex; - set => Head.FaceAttachmentIndex = value; - } - - public readonly ImmutableArray<(Color Color, float Commonness)> HairColors; - public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors; - public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors; - - public Color HairColor - { - get => Head.HairColor; - set => Head.HairColor = value; - } - - public Color FacialHairColor - { - get => Head.FacialHairColor; - set => Head.FacialHairColor = value; - } - - public Color SkinColor - { - get => Head.SkinColor; - set => Head.SkinColor = value; - } - - public XElement HairElement => Head.HairElement; - - public XElement BeardElement => Head.BeardElement; - - public XElement MoustacheElement => Head.MoustacheElement; - - public XElement FaceAttachment => Head.FaceAttachment; + public readonly bool HasSpecifierTags; private RagdollParams ragdoll; public RagdollParams Ragdoll @@ -534,8 +407,8 @@ namespace Barotrauma if (ragdoll == null) { // TODO: support for variants - string speciesName = SpeciesName; - bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)); + Identifier speciesName = SpeciesName; + bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName == CharacterPrefab.HumanSpeciesName); ragdoll = isHumanoid ? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName) : RagdollParams.GetRagdollParams(speciesName, ragdollFileName) as RagdollParams; @@ -545,119 +418,171 @@ namespace Barotrauma set { ragdoll = value; } } - public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; + public bool IsAttachmentsLoaded => Head.HairIndex > -1 && Head.BeardIndex > -1 && Head.MoustacheIndex > -1 && Head.FaceAttachmentIndex > -1; + + public IEnumerable GetValidAttachmentElements(IEnumerable elements, HeadPreset headPreset, WearableType? wearableType = null) + => FilterElements(elements, headPreset.TagSet, wearableType); + + public int CountValidAttachmentsOfType(WearableType wearableType) + => GetValidAttachmentElements(Wearables, Head.Preset, wearableType).Count(); + + public readonly ImmutableArray<(Color Color, float Commonness)> HairColors; + public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors; + public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors; + + private void GetName(ContentPath namesFile, Rand.RandSync randSync, out string name) + { + XDocument doc = XMLExtensions.TryLoadXml(namesFile); + name = doc.Root.GetAttributeString("format", ""); + Dictionary> entries = new Dictionary>(); + foreach (var subElement in doc.Root.Elements()) + { + Identifier elemName = subElement.NameAsIdentifier(); + if (!entries.ContainsKey(elemName)) + { + entries.Add(elemName, new List()); + } + ImmutableHashSet identifiers = subElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); + if (identifiers.IsSubsetOf(Head.Preset.TagSet)) + { + entries[elemName].Add(subElement.GetAttributeString("value", "")); + } + } + + foreach (var k in entries.Keys) + { + name = name.Replace($"[{k}]", entries[k].GetRandom(randSync), StringComparison.OrdinalIgnoreCase); + } + } + + private static void LoadTagsBackwardsCompatibility(XElement element, HashSet tags) + { + //we need this to be able to load save files from + //older versions with the shittier hardcoded character + //info implementation + Identifier gender = element.GetAttributeIdentifier("gender", ""); + int headSpriteId = element.GetAttributeInt("headspriteid", -1); + if (!gender.IsEmpty) { tags.Add(gender); } + if (headSpriteId > 0) { tags.Add($"head{headSpriteId}".ToIdentifier()); } + } // talent-relevant values public int MissionsCompletedSinceDeath = 0; // Used for creating the data - public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, string npcIdentifier = "") + public CharacterInfo(Identifier speciesName, string name = "", string originalName = "", Either jobOrJobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, Identifier npcIdentifier = default) { - if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + JobPrefab jobPrefab = null; + Job job = null; + if (jobOrJobPrefab != null) { - speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); + jobOrJobPrefab.TryGet(out job); + jobOrJobPrefab.TryGet(out jobPrefab); } ID = idCounter; idCounter++; - _speciesName = speciesName; - SpriteTags = new List(); - XDocument doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument; - if (doc == null) { return; } - CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + SpeciesName = speciesName; + SpriteTags = new List(); + CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement; + if (CharacterConfigElement == null) { return; } // TODO: support for variants - Head = new HeadInfo(); - HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - HasRaces = CharacterConfigElement.GetAttributeBool("races", false); - SetGenderAndRace(randSync); - Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant); - HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - SetColors(); + HasSpecifierTags = CharacterConfigElement.GetAttributeBool("specifiertags", false); + if (HasSpecifierTags) + { + HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - if (!string.IsNullOrEmpty(name)) - { - Name = name; - } - else if (!string.IsNullOrEmpty(npcIdentifier) && TextManager.Get("npctitle." + npcIdentifier, true) is string npcTitle) - { - Name = npcTitle; - } - else - { - Name = GetRandomName(randSync); + var headPreset = Prefab.Heads.GetRandom(randSync); + Head = new HeadInfo(this, headPreset); + SetAttachments(randSync); + SetColors(randSync); + + Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant)); + + if (!string.IsNullOrEmpty(name)) + { + Name = name; + } + else if (!npcIdentifier.IsEmpty && TextManager.Get("npctitle." + npcIdentifier) is { Loaded: true } npcTitle) + { + Name = npcTitle.Value; + } + else + { + Name = GetRandomName(randSync); + } + + SetPersonalityTrait(); + + Salary = CalculateSalary(); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; - SetPersonalityTrait(); - Salary = CalculateSalary(); if (ragdollFileName != null) { this.ragdollFileName = ragdollFileName; } - LoadHeadAttachments(); } private void SetPersonalityTrait() - { - personalityTrait = NPCPersonalityTrait.GetRandom(Name + HeadSpriteId); - } + => PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet)); public string GetRandomName(Rand.RandSync randSync) { string name = ""; - if (CharacterConfigElement.Element("name") != null) + var nameElement = CharacterConfigElement.GetChildElement("names"); + if (nameElement != null) { - string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); - if (firstNamePath != "") - { - firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - name = ToolBox.GetRandomLine(firstNamePath, randSync); - } - - string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); - if (lastNamePath != "") - { - lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - if (name != "") { name += " "; } - name += ToolBox.GetRandomLine(lastNamePath, randSync); - } + GetName(nameElement.GetAttributeContentPath("path") ?? ContentPath.Empty, randSync, out name); } return name; } - public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array) - => ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), Rand.RandSync.Unsynced) + public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array, Rand.RandSync randSync) + => ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), randSync) .Color; - private void SetGenderAndRace(Rand.RandSync randSync) + private void SetAttachments(Rand.RandSync randSync) { - Head.gender = GetRandomGender(randSync); - Head.race = GetRandomRace(randSync); - CalculateHeadSpriteRange(); - HeadSpriteId = GetRandomHeadID(randSync); + LoadHeadAttachments(); + + int pickRandomIndex(IReadOnlyList list) + { + var elems = GetValidAttachmentElements(list, Head.Preset).ToArray(); + var weights = GetWeights(elems).ToArray(); + return list.IndexOf(ToolBox.SelectWeightedRandom(elems, weights, randSync)); + } + + Head.HairIndex = pickRandomIndex(Hairs); + Head.BeardIndex = pickRandomIndex(Beards); + Head.MoustacheIndex = pickRandomIndex(Moustaches); + Head.FaceAttachmentIndex = pickRandomIndex(FaceAttachments); } - private void SetColors() + private void SetColors(Rand.RandSync randSync) { - HairColor = SelectRandomColor(HairColors); - FacialHairColor = SelectRandomColor(FacialHairColors); - SkinColor = SelectRandomColor(SkinColors); + Head.HairColor = SelectRandomColor(HairColors, randSync); + Head.FacialHairColor = SelectRandomColor(FacialHairColors, randSync); + Head.SkinColor = SelectRandomColor(SkinColors, randSync); } + private bool IsColorValid(in Color clr) + => clr.R != 0 || clr.G != 0 || clr.B != 0; + private void CheckColors() { - if (HairColor == Color.Black) + if (IsColorValid(Head.HairColor)) { - HairColor = SelectRandomColor(HairColors); + Head.HairColor = SelectRandomColor(HairColors, Rand.RandSync.Unsynced); } - if (FacialHairColor == Color.Black) + if (IsColorValid(Head.FacialHairColor)) { - FacialHairColor = SelectRandomColor(FacialHairColors); + Head.FacialHairColor = SelectRandomColor(FacialHairColors, Rand.RandSync.Unsynced); } - if (SkinColor == Color.Black) + if (IsColorValid(Head.SkinColor)) { - SkinColor = SelectRandomColor(SkinColors); + Head.SkinColor = SelectRandomColor(SkinColors, Rand.RandSync.Unsynced); } } @@ -669,97 +594,52 @@ namespace Barotrauma Name = infoElement.GetAttributeString("name", ""); OriginalName = infoElement.GetAttributeString("originalname", null); Salary = infoElement.GetAttributeInt("salary", 1000); + ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); - UnlockedTalents = new HashSet(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true)); + UnlockedTalents = new HashSet(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty())); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); - Enum.TryParse(infoElement.GetAttributeString("race", "None"), true, out Race race); - Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender); - _speciesName = infoElement.GetAttributeString("speciesname", null); - XDocument doc = null; - if (_speciesName != null) + HashSet tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); + LoadTagsBackwardsCompatibility(infoElement, tags); + SpeciesName = infoElement.GetAttributeIdentifier("speciesname", ""); + ContentXElement element; + if (!SpeciesName.IsEmpty) { - doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument; + element = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement; } else { // Backwards support (human only) - string file = infoElement.GetAttributeString("file", ""); - doc = XMLExtensions.TryLoadXml(file); + // Actually you know what this is backwards! + throw new InvalidOperationException("SpeciesName not defined"); } - if (doc == null) { return; } + if (element == null) { return; } // TODO: support for variants - CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - HasRaces = CharacterConfigElement.GetAttributeBool("hasraces", false); - if (!IsValidGender(gender)) + CharacterConfigElement = element; + HasSpecifierTags = CharacterConfigElement.GetAttributeBool("specifiertags", false); + if (HasSpecifierTags) { - gender = GetRandomGender(Rand.RandSync.Unsynced); - } - if (!IsValidRace(race)) - { - race = GetRandomRace(Rand.RandSync.Unsynced); - } - HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - - RecreateHead( - infoElement.GetAttributeInt("headspriteid", 1), - race, - gender, - infoElement.GetAttributeInt("hairindex", -1), - infoElement.GetAttributeInt("beardindex", -1), - infoElement.GetAttributeInt("moustacheindex", -1), - infoElement.GetAttributeInt("faceattachmentindex", -1)); + RecreateHead( + tags.ToImmutableHashSet(), + infoElement.GetAttributeInt("hairindex", -1), + infoElement.GetAttributeInt("beardindex", -1), + infoElement.GetAttributeInt("moustacheindex", -1), + infoElement.GetAttributeInt("faceattachmentindex", -1)); - //backwards compatibility - if (infoElement.Attribute("skincolor") == null && infoElement.Attribute("race") != null) - { - string raceStr = infoElement.GetAttributeString("race", string.Empty); - Race obsoleteRace = Race.None; - Enum.TryParse(raceStr, ignoreCase: true, out obsoleteRace); - switch (obsoleteRace) + HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); + + Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White); + Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White); + Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); + CheckColors(); + + if (string.IsNullOrEmpty(Name)) { - case Race.White: - case Race.None: - SkinColor = new Color(255, 215, 200, 255); - break; - case Race.Brown: - SkinColor = new Color(158, 95, 72, 255); - break; - case Race.Black: - SkinColor = new Color(153, 75, 42, 255); - break; - case Race.Asian: - SkinColor = new Color(191, 116, 61, 255); - break; - } - } - else - { - SkinColor = infoElement.GetAttributeColor("skincolor", Color.Black); - } - HairColor = infoElement.GetAttributeColor("haircolor", Color.Black); - FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Black); - CheckColors(); - - if (string.IsNullOrEmpty(Name)) - { - if (CharacterConfigElement.Element("name") != null) - { - string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); - if (firstNamePath != "") + var nameElement = CharacterConfigElement.GetChildElement("names"); + if (nameElement != null) { - firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - Name = ToolBox.GetRandomLine(firstNamePath); - } - - string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); - if (lastNamePath != "") - { - lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - if (Name != "") Name += " "; - Name += ToolBox.GetRandomLine(lastNamePath); + GetName(nameElement.GetAttributeContentPath("path") ?? ContentPath.Empty, Rand.RandSync.ServerAndClient, out Name); } } } @@ -770,16 +650,16 @@ namespace Barotrauma } StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false); - string personalityName = infoElement.GetAttributeString("personality", ""); + Identifier personalityName = infoElement.GetAttributeIdentifier("personality", ""); ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty); - if (!string.IsNullOrEmpty(personalityName)) + if (personalityName != Identifier.Empty) { - personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName); + PersonalityTrait = NPCPersonalityTrait.Get(GameSettings.CurrentConfig.Language, personalityName); } MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); - foreach (XElement subElement in infoElement.Elements()) + foreach (var subElement in infoElement.Elements()) { bool jobCreated = false; if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated) @@ -818,43 +698,26 @@ namespace Barotrauma LoadHeadAttachments(); } - public Gender GetRandomGender(Rand.RandSync randSync) - { - if (HasGenders) - { - return (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; - } - return Gender.None; - } + private List hairs; + public IReadOnlyList Hairs => hairs; + private List beards; + public IReadOnlyList Beards => beards; + private List moustaches; + public IReadOnlyList Moustaches => moustaches; + private List faceAttachments; + public IReadOnlyList FaceAttachments => faceAttachments; - public Race GetRandomRace(Rand.RandSync randSync) - { - if (HasRaces) - { - return new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync); - } - return Race.None; - } - - - public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0; - - private List hairs; - private List beards; - private List moustaches; - private List faceAttachments; - - private IEnumerable wearables; - public IEnumerable Wearables + private IEnumerable wearables; + public IEnumerable Wearables { get { if (wearables == null) { - var attachments = CharacterConfigElement.Element("HeadAttachments"); + var attachments = CharacterConfigElement.GetChildElement("HeadAttachments"); if (attachments != null) { - wearables = attachments.Elements("Wearable"); + wearables = attachments.GetChildElements("Wearable"); } } return wearables; @@ -873,167 +736,77 @@ namespace Barotrauma private int GetIdentifier(string name) { - int id = ToolBox.StringToInt(name); - id ^= HeadSpriteId; - id ^= (int)Race << 6; - id ^= HairIndex << 12; - id ^= BeardIndex << 18; - id ^= MoustacheIndex << 24; - id ^= FaceAttachmentIndex << 30; + int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet)); + id ^= Head.HairIndex << 12; + id ^= Head.BeardIndex << 18; + id ^= Head.MoustacheIndex << 24; + id ^= Head.FaceAttachmentIndex << 30; if (Job != null) { - id ^= ToolBox.StringToInt(Job.Prefab.Identifier); + id ^= ToolBox.StringToInt(Job.Prefab.Identifier.Value); } return id; } - public IEnumerable FilterByTypeAndHeadID(IEnumerable elements, WearableType targetType, int headSpriteId) + public IEnumerable FilterElements(IEnumerable elements, ImmutableHashSet tags, WearableType? targetType = null) { - if (elements == null) { return elements; } - return elements.Where(e => + if (elements is null) { return null; } + return elements.Where(w => { - if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; } - int headId = e.GetAttributeInt("headid", -1); - // if the head id is less than 1, the id is not valid and the condition is ignored. - return headId < 1 || headId == headSpriteId; + if (!(targetType is null)) + { + if (Enum.TryParse(w.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; } + } + HashSet t = w.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); + LoadTagsBackwardsCompatibility(w, t); + return t.IsSubsetOf(tags); }); } - public IEnumerable FilterElementsByGenderAndRace(IEnumerable elements, Gender gender, Race race) + public void RecreateHead(ImmutableHashSet tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) { - if (elements == null) { return elements; } - return elements.Where(w => - IsMatchingGender(Enum.Parse(w.GetAttributeString("gender", "None"), ignoreCase: true), gender) && - IsMatchingRace(Enum.Parse(w.GetAttributeString("race", "None"), ignoreCase: true), race)); + HeadPreset headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags)); + if (headPreset == null) { headPreset = Prefab.Heads.GetRandomUnsynced(); } + head = new HeadInfo(this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + ReloadHeadAttachments(); } - public static bool IsMatchingGender(Gender gender, Gender myGender) => gender == Gender.None || gender == myGender; - public static bool IsMatchingRace(Race race, Race myRace) => race == Race.None || race == myRace; - - private void LoadHeadPresets() + public string ReplaceVars(string str) { - if (CharacterConfigElement == null) { return; } - heads = new Dictionary(); - var headsElement = CharacterConfigElement.GetChildElement("heads"); - if (headsElement != null) - { - foreach (var head in headsElement.GetChildElements("head")) - { - var preset = new HeadPreset(head); - heads.Add(preset, preset.SheetIndex); - } - } + return Prefab.ReplaceVars(str, Head.Preset); } - private void CalculateHeadSpriteRange() +#if CLIENT + public void RecreateHead(MultiplayerPreferences characterSettings) { - if (CharacterConfigElement == null) { return; } - Head.headSpriteRange = CharacterConfigElement.GetAttributeVector2("headidrange", Vector2.Zero); - // If the range is defined, we use it as it is - if (Head.headSpriteRange != Vector2.Zero) { return; } - if (heads == null) - { - LoadHeadPresets(); - } - // If there are any head presets defined, use them. - if (heads.Any()) - { - var ids = heads.Keys.Where(h => IsMatchingRace(Race, h.Race) && IsMatchingGender(Gender, h.Gender)).Select(w => w.ID); - ids = ids.OrderBy(id => id); - if (ids.Any()) - { - Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); - } - else - { - DebugConsole.ThrowError($"[CharacterInfo] Couldn't find a head definition that matches {Race} and {Gender}!"); - } - } - // Else we calculate the range from the wearables. - if (Head.headSpriteRange == Vector2.Zero) - { - var wearableElements = Wearables; - if (wearableElements == null) { return; } - var wearables = FilterElementsByGenderAndRace(wearableElements, head.gender, head.race).ToList(); - if (wearables == null) - { - Head.headSpriteRange = Vector2.Zero; - return; - } - if (wearables.None()) - { - DebugConsole.ThrowError($"[CharacterInfo] No headidrange defined and no wearables matching the gender {Head.gender} and the race {Head.race} could be found. Total wearables found: {Wearables.Count()}."); - return; - } - else - { - // Ignore head ids that are less than 1, because they are not supported. - var ids = wearables.Select(w => w.GetAttributeInt("headid", -1)).Where(id => id > 0); - if (ids.None()) - { - DebugConsole.ThrowError($"[CharacterInfo] Wearables with matching gender and race were found but none with a valid headid! Total wearables found: {Wearables.Count()}."); - return; - } - ids = ids.OrderBy(id => id); - Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); - } - } - } + RecreateHead( + characterSettings.TagSet.ToImmutableHashSet(), + characterSettings.HairIndex, + characterSettings.BeardIndex, + characterSettings.MoustacheIndex, + characterSettings.FaceAttachmentIndex); + Head.SkinColor = characterSettings.SkinColor; + Head.HairColor = characterSettings.HairColor; + Head.FacialHairColor = characterSettings.FacialHairColor; + } +#endif + public void RecreateHead(HeadInfo headInfo) { RecreateHead( - headInfo.HeadSpriteId, - headInfo.race, - headInfo.gender, + headInfo.Preset.TagSet, headInfo.HairIndex, headInfo.BeardIndex, headInfo.MoustacheIndex, headInfo.FaceAttachmentIndex); - SkinColor = headInfo.SkinColor; - HairColor = headInfo.HairColor; - FacialHairColor = headInfo.FacialHairColor; + Head.SkinColor = headInfo.SkinColor; + Head.HairColor = headInfo.HairColor; + Head.FacialHairColor = headInfo.FacialHairColor; CheckColors(); } - /// - /// Recreates the head info and checks that everything is valid. - /// - public void RecreateHead(int headID, Race race, Gender gender, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) - { - if (!IsValidGender(gender)) - { - gender = GetRandomGender(Rand.RandSync.Unsynced); - } - if (!IsValidRace(race)) - { - race = GetRandomRace(Rand.RandSync.Unsynced); - } - if (heads == null) - { - LoadHeadPresets(); - } - Color skin = Color.Black; - Color hair = Color.Black; - Color facialHair = Color.Black; - if (head != null) - { - skin = head.SkinColor; - hair = head.HairColor; - facialHair = head.FacialHairColor; - } - head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex) - { - SkinColor = skin, - HairColor = hair, - FacialHairColor = facialHair - }; - CalculateHeadSpriteRange(); - ReloadHeadAttachments(); - RefreshHead(); - } - /// /// Reloads the head sprite and the attachment sprites. /// @@ -1043,23 +816,21 @@ namespace Barotrauma RefreshHeadSprites(); } - partial void LoadHeadSpriteProjectSpecific(XElement limbElement); + partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement); private void LoadHeadSprite() { - foreach (XElement limbElement in Ragdoll.MainElement.Elements()) + foreach (var limbElement in Ragdoll.MainElement.Elements()) { if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } - XElement spriteElement = limbElement.Element("sprite"); + ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; if (string.IsNullOrEmpty(spritePath)) { continue; } - spritePath = spritePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - spritePath = spritePath.Replace("[RACE]", Head.race.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString()); + spritePath = ReplaceVars(spritePath); string fileName = Path.GetFileNameWithoutExtension(spritePath); @@ -1080,7 +851,7 @@ namespace Barotrauma Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; //extract the tags out of the filename - SpriteTags = file.Split('[', ']').Skip(1).ToList(); + SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList(); if (SpriteTags.Any()) { SpriteTags.RemoveAt(SpriteTags.Count - 1); @@ -1101,95 +872,44 @@ namespace Barotrauma { if (hairs == null) { - float commonness = Gender == Gender.Female ? 0.05f : 0.2f; - hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Hair, head.HeadSpriteId), WearableType.Hair, commonness); + float commonness = 0.1f; + hairs = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Hair), WearableType.Hair, commonness); } if (beards == null) { - beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Beard, head.HeadSpriteId), WearableType.Beard); + beards = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Beard), WearableType.Beard); } if (moustaches == null) { - moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Moustache, head.HeadSpriteId), WearableType.Moustache); + moustaches = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Moustache), WearableType.Moustache); } if (faceAttachments == null) { - faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.FaceAttachment, head.HeadSpriteId), WearableType.FaceAttachment); - } - - if (IsValidIndex(Head.HairIndex, hairs)) - { - Head.HairElement = hairs[Head.HairIndex]; - } - else - { - Head.HairElement = GetRandomElement(hairs); - Head.HairIndex = hairs.IndexOf(Head.HairElement); - } - if (Head.HairElement != null) - { - int thisHairIndex = hairs.IndexOf(head.HairElement); - int hairWithHatIndex = head.HairElement.GetAttributeInt("replacewhenwearinghat", thisHairIndex); - if (thisHairIndex != hairWithHatIndex && hairWithHatIndex > -1 && hairWithHatIndex < hairs.Count) - { - head.HairWithHatElement = hairs[hairWithHatIndex]; - } - else - { - head.HairWithHatElement = null; - } - } - - if (IsValidIndex(Head.BeardIndex, beards)) - { - Head.BeardElement = beards[Head.BeardIndex]; - } - else - { - Head.BeardElement = GetRandomElement(beards); - Head.BeardIndex = beards.IndexOf(Head.BeardElement); - } - if (IsValidIndex(Head.MoustacheIndex, moustaches)) - { - Head.MoustacheElement = moustaches[Head.MoustacheIndex]; - } - else - { - Head.MoustacheElement = GetRandomElement(moustaches); - Head.MoustacheIndex = moustaches.IndexOf(Head.MoustacheElement); - } - if (IsValidIndex(Head.FaceAttachmentIndex, faceAttachments)) - { - Head.FaceAttachment = faceAttachments[Head.FaceAttachmentIndex]; - } - else - { - Head.FaceAttachment = GetRandomElement(faceAttachments); - Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment); + faceAttachments = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.FaceAttachment), WearableType.FaceAttachment); } } } - public static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) + public static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) { // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example. - var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)); - var list = new List() { emptyElement }; + var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)).FromPackage(null); + var list = new List() { emptyElement }; list.AddRange(elements); return list; } - public XElement GetRandomElement(IEnumerable elements) + public ContentXElement GetRandomElement(IEnumerable elements) { var filtered = elements.Where(IsWearableAllowed); if (filtered.Count() == 0) { return null; } var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced); - return element == null || element.Name == "Empty" ? null : element; + return element == null || element.NameAsIdentifier() == "Empty" ? null : element; } - private bool IsWearableAllowed(XElement element) + private bool IsWearableAllowed(ContentXElement element) { - string spriteName = element.Element("sprite").GetAttributeString("name", string.Empty); + string spriteName = element.GetChildElement("sprite").GetAttributeString("name", string.Empty); return IsAllowed(Head.HairElement, spriteName) && IsAllowed(Head.BeardElement, spriteName) && IsAllowed(Head.MoustacheElement, spriteName) && IsAllowed(Head.FaceAttachment, spriteName); } @@ -1197,7 +917,7 @@ namespace Barotrauma { if (element != null) { - var disallowed = element.GetAttributeStringArray("disallow", new string[0]); + var disallowed = element.GetAttributeStringArray("disallow", Array.Empty()); if (disallowed.Any(s => spriteName.Contains(s))) { return false; @@ -1206,9 +926,9 @@ namespace Barotrauma return true; } - public static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; + public static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; - private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); + private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); partial void LoadAttachmentSprites(bool omitJob); @@ -1225,9 +945,9 @@ namespace Barotrauma return (int)(salary * Job.Prefab.PriceMultiplier); } - public void IncreaseSkillLevel(string skillIdentifier, float increase, bool gainedFromAbility = false) + public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false) { - if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } + if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } if (Job.Prefab.Identifier == "assistant") { @@ -1256,7 +976,7 @@ namespace Barotrauma OnSkillChanged(skillIdentifier, prevLevel, newLevel); } - public void SetSkillLevel(string skillIdentifier, float level) + public void SetSkillLevel(Identifier skillIdentifier, float level) { if (Job == null) { return; } @@ -1274,7 +994,7 @@ namespace Barotrauma } } - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel); + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel); public void GiveExperience(int amount, bool isMissionExperience = false) { @@ -1393,24 +1113,22 @@ namespace Barotrauma new XAttribute("name", Name), new XAttribute("originalname", OriginalName), new XAttribute("speciesname", SpeciesName), - new XAttribute("gender", Head.gender.ToString()), - new XAttribute("race", Head.race.ToString()), + new XAttribute("tags", string.Join(",", Head.Preset.TagSet)), new XAttribute("salary", Salary), new XAttribute("experiencepoints", ExperiencePoints), new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)), new XAttribute("additionaltalentpoints", AdditionalTalentPoints), - new XAttribute("headspriteid", HeadSpriteId), - new XAttribute("hairindex", HairIndex), - new XAttribute("beardindex", BeardIndex), - new XAttribute("moustacheindex", MoustacheIndex), - new XAttribute("faceattachmentindex", FaceAttachmentIndex), - new XAttribute("skincolor", XMLExtensions.ColorToString(SkinColor)), - new XAttribute("haircolor", XMLExtensions.ColorToString(HairColor)), - new XAttribute("facialhaircolor", XMLExtensions.ColorToString(FacialHairColor)), + new XAttribute("hairindex", Head.HairIndex), + new XAttribute("beardindex", Head.BeardIndex), + new XAttribute("moustacheindex", Head.MoustacheIndex), + new XAttribute("faceattachmentindex", Head.FaceAttachmentIndex), + new XAttribute("skincolor", XMLExtensions.ColorToString(Head.SkinColor)), + new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)), + new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), - new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name)); - // TODO: animations? + new XAttribute("personality", PersonalityTrait?.Name.Value ?? "")); + // TODO: animations? charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); @@ -1448,7 +1166,7 @@ namespace Barotrauma return charElement; } - public static void SaveOrders(XElement parentElement, params OrderInfo[] orders) + public static void SaveOrders(XElement parentElement, params Order[] orders) { if (parentElement == null || orders == null || orders.None()) { return; } // If an order is invalid, we discard the order and increase the priority of the following orders so @@ -1458,8 +1176,8 @@ namespace Barotrauma var linkedSubs = GetLinkedSubmarines(); foreach (var orderInfo in orders) { - var order = orderInfo.Order; - if (order == null || string.IsNullOrEmpty(order.Identifier)) + var order = orderInfo; + if (order == null || order.Identifier == Identifier.Empty) { DebugConsole.ThrowError("Error saving an order - the order or its identifier is null"); priorityIncrease++; @@ -1488,7 +1206,7 @@ namespace Barotrauma targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); if (!targetAvailableInNextLevel) { - if (!order.CanBeGeneralized) + if (!order.Prefab.CanBeGeneralized) { DebugConsole.Log($"Trying to save an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order requires a target so it won't be saved."); priorityIncrease++; @@ -1510,9 +1228,9 @@ namespace Barotrauma new XAttribute("id", order.Identifier), new XAttribute("priority", orderInfo.ManualPriority + priorityIncrease), new XAttribute("targettype", (int)order.TargetType)); - if (!string.IsNullOrEmpty(orderInfo.OrderOption)) + if (orderInfo.Option != Identifier.Empty) { - orderElement.Add(new XAttribute("option", orderInfo.OrderOption)); + orderElement.Add(new XAttribute("option", orderInfo.Option)); } if (order.OrderGiver != null) { @@ -1559,7 +1277,7 @@ namespace Barotrauma /// public static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement) { - var currentOrders = new List(characterInfo.CurrentOrders); + var currentOrders = new List(characterInfo.CurrentOrders); // Sort the current orders to make sure the one with the highest priority comes first currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); SaveOrders(parentElement, currentOrders.ToArray()); @@ -1580,7 +1298,7 @@ namespace Barotrauma var orders = LoadOrders(orderData); foreach (var order in orders) { - character.SetOrder(order, order.Order?.OrderGiver, speak: false, force: true); + character.SetOrder(order, speak: false, force: true); } } @@ -1589,9 +1307,9 @@ namespace Barotrauma ApplyOrderData(Character, OrderData); } - public static List LoadOrders(XElement ordersElement) + public static List LoadOrders(XElement ordersElement) { - var orders = new List(); + var orders = new List(); if (ordersElement == null) { return orders; } // If an order is invalid, we discard the order and increase the priority of the following orders so // 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and @@ -1602,7 +1320,7 @@ namespace Barotrauma { Order order = null; string orderIdentifier = orderElement.GetAttributeString("id", ""); - var orderPrefab = Order.GetPrefab(orderIdentifier); + var orderPrefab = OrderPrefab.Prefabs[orderIdentifier]; if (orderPrefab == null) { DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\""); @@ -1648,9 +1366,9 @@ namespace Barotrauma order = new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver); break; } - string orderOption = orderElement.GetAttributeString("option", ""); + Identifier orderOption = orderElement.GetAttributeIdentifier("option", ""); int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease; - var orderInfo = new OrderInfo(order, orderOption, manualPriority); + var orderInfo = order.WithOption(orderOption).WithManualPriority(manualPriority); orders.Add(orderInfo); bool GetTargetEntity(ushort targetId, out Entity targetEntity) @@ -1722,21 +1440,12 @@ namespace Barotrauma /// /// Reloads the attachment xml elements according to the indices. Doesn't reload the sprites. /// - private void ReloadHeadAttachments() + public void ReloadHeadAttachments() { ResetLoadedAttachments(); LoadHeadAttachments(); } - /// - /// Loads only the elements according to the indices, not the sprites. - /// - private void ResetHeadAttachments() - { - ResetAttachmentIndices(); - ResetLoadedAttachments(); - } - private void ResetAttachmentIndices() { Head.ResetAttachmentIndices(); @@ -1828,11 +1537,11 @@ namespace Barotrauma return 0f; } } - public float GetSavedStatValue(StatTypes statType, string statIdentifier) + public float GetSavedStatValue(StatTypes statType, Identifier statIdentifier) { if (SavedStatValues.TryGetValue(statType, out var statValues)) { - return statValues.Where(s => s.StatIdentifier.Equals(statIdentifier, StringComparison.OrdinalIgnoreCase)).Sum(v => v.StatValue); + return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue); } else { @@ -1879,7 +1588,7 @@ namespace Barotrauma class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter { - public AbilitySkillGain(float skillAmount, string skillIdentifier, Character character, bool gainedFromAbility) + public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility) { Value = skillAmount; SkillIdentifier = skillIdentifier; @@ -1888,7 +1597,7 @@ namespace Barotrauma } public Character Character { get; set; } public float Value { get; set; } - public string SkillIdentifier { get; set; } + public Identifier SkillIdentifier { get; set; } public bool GainedFromAbility { get; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index e5802eae4..6cec0f84c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; +using static Barotrauma.CharacterInfo; namespace Barotrauma { - class CharacterPrefab : IPrefab, IDisposable + class CharacterPrefab : PrefabWithUintIdentifier, IImplementsVariants { public readonly static PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; @@ -19,36 +21,51 @@ namespace Barotrauma Character.RemoveByPrefab(this); } - public string OriginalName { get; private set; } - public string Name { get; private set; } - public string Identifier { get; private set; } - public string FilePath { get; private set; } - public string VariantOf { get; private set; } + public string Name => Identifier.Value; + public Identifier VariantOf { get; } + public void InheritFrom(CharacterPrefab parent) + { + ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ParseConfigElement(); + } - public ContentPackage ContentPackage { get; private set; } + private void ParseConfigElement() + { + var headsElement = ConfigElement.GetChildElement("Heads"); + var varsElement = ConfigElement.GetChildElement("Vars"); + var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory"); + var pronounsElement = ConfigElement.GetChildElement("Pronouns"); - public XDocument XDocument { get; private set; } + if (headsElement != null && varsElement != null && menuCategoryElement != null && pronounsElement != null) + { + CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement); + } + } - public static IEnumerable ConfigFilePaths => Prefabs.Select(p => p.FilePath); - public static IEnumerable ConfigFiles => Prefabs.Select(p => p.XDocument); + private XElement originalElement; + public ContentXElement ConfigElement { get; private set; } - public const string HumanSpeciesName = "human"; - public static string HumanConfigFile => FindBySpeciesName(HumanSpeciesName).FilePath; + public CharacterInfoPrefab CharacterInfoPrefab { get; private set; } + + public static IEnumerable ConfigElements => Prefabs.Select(p => p.ConfigElement); + + public static readonly Identifier HumanSpeciesName = "human".ToIdentifier(); + public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; + public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); /// /// Searches for a character config file from all currently selected content packages, /// or from a specific package if the contentPackage parameter is given. /// - public static CharacterPrefab FindBySpeciesName(string speciesName) + public static CharacterPrefab FindBySpeciesName(Identifier speciesName) { - speciesName = speciesName.ToLowerInvariant(); if (!Prefabs.ContainsKey(speciesName)) { return null; } return Prefabs[speciesName]; } public static CharacterPrefab FindByFilePath(string filePath) { - return Prefabs.Find(p => p.FilePath.CleanUpPath() == filePath.CleanUpPath()); + return Prefabs.Find(p => p.ContentFile.Path == filePath); } public static CharacterPrefab Find(Predicate predicate) @@ -56,91 +73,38 @@ namespace Barotrauma return Prefabs.Find(predicate); } - public static void RemoveByFile(string file) + public CharacterPrefab(ContentXElement mainElement, CharacterFile file) : base(file, ParseName(mainElement, file)) { - Prefabs.RemoveByFile(file); + originalElement = mainElement; + ConfigElement = mainElement; + VariantOf = mainElement.VariantOf(); + + ParseConfigElement(); } - public static bool LoadFromFile(ContentFile file, bool forceOverride=false) + public static Identifier ParseName(XElement element, CharacterFile file) { - return LoadFromFile(file.Path, file.ContentPackage, forceOverride); - } - - public static bool LoadFromFile(string filePath, ContentPackage contentPackage, bool forceOverride=false) - { - XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null) + string name = element.GetAttributeString("name", null); + if (!string.IsNullOrEmpty(name)) { - DebugConsole.ThrowError($"Loading character file failed: {filePath}"); - return false; - } - if (Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.FilePath == filePath))) - { - DebugConsole.ThrowError($"Duplicate path: {filePath}"); - return false; - } - XElement mainElement = doc.Root; - if (doc.Root.IsCharacterVariant()) - { - if (!CheckSpeciesName(mainElement, filePath, out string n)) { return false; } - string inherit = mainElement.GetAttributeString("inherit", null); - string id = n.ToLowerInvariant(); - Prefabs.Add(new CharacterPrefab - { - Name = n, - OriginalName = n, - Identifier = id, - FilePath = filePath, - ContentPackage = contentPackage, - XDocument = doc, - VariantOf = inherit - }, isOverride: false); - return true; - } - else if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - } - if (!CheckSpeciesName(mainElement, filePath, out string name)) { return false; } - string identifier = name.ToLowerInvariant(); - Prefabs.Add(new CharacterPrefab - { - Name = name, - OriginalName = name, - Identifier = identifier, - FilePath = filePath, - ContentPackage = contentPackage, - XDocument = doc - }, forceOverride || doc.Root.IsOverride()); - - return true; - } - - public static bool CheckSpeciesName(XElement mainElement, string filePath, out string name) - { - name = mainElement.GetAttributeString("name", null); - if (name != null) - { - DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); + DebugConsole.NewMessage($"Error in {file.Path}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); } else { - name = mainElement.GetAttributeString("speciesname", string.Empty); + name = element.GetAttributeString("speciesname", string.Empty); } - if (string.IsNullOrWhiteSpace(name)) + return new Identifier(name); + } + + public static bool CheckSpeciesName(XElement mainElement, CharacterFile file, out Identifier name) + { + name = ParseName(mainElement, file); + if (name == Identifier.Empty) { - DebugConsole.ThrowError($"No species name defined for: {filePath}"); + DebugConsole.ThrowError($"No species name defined for: {file.Path}"); return false; } return true; } - - public static void LoadAll() - { - foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character)) - { - LoadFromFile(file); - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs index 070190f32..3a415130a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs @@ -7,19 +7,19 @@ using System.Xml.Linq; namespace Barotrauma { - class CorpsePrefab : HumanPrefab, IPrefab, IDisposable + class CorpsePrefab : HumanPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; Prefabs.Remove(this); } - public static CorpsePrefab Get(string identifier) + public static CorpsePrefab Get(Identifier identifier) { if (Prefabs == null) { @@ -37,99 +37,11 @@ namespace Barotrauma } } - [Serialize(Level.PositionType.Wreck, false)] + [Serialize(Level.PositionType.Wreck, IsPropertySaveable.No)] public Level.PositionType SpawnPosition { get; private set; } - public ContentPackage ContentPackage { get; private set; } - - public CorpsePrefab(XElement element, string filePath, bool allowOverriding) : base(element, filePath) - { - Prefabs.Add(this, allowOverriding); - } + public CorpsePrefab(ContentXElement element, CorpsesFile file) : base(element, file) { } public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync); - - public static void LoadAll(IEnumerable files) - { - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - DebugConsole.Log("*** " + file.Path + " ***"); - RemoveByFile(file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - var rootElement = doc.Root; - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "corpse": - new CorpsePrefab(rootElement, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - break; - case "corpses": - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var itemElement = element.GetChildElement("item"); - if (itemElement != null) - { - new CorpsePrefab(itemElement, file.Path, true) - { - ContentPackage = file.ContentPackage - }; - } - else - { - DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}"); - } - } - else - { - new CorpsePrefab(element, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - } - } - break; - case "override": - var corpses = rootElement.GetChildElement("corpses"); - if (corpses != null) - { - foreach (var element in corpses.Elements()) - { - new CorpsePrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage, - }; - } - } - foreach (var element in rootElement.GetChildElements("corpse")) - { - new CorpsePrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage - }; - } - break; - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}"); - break; - } - } - - public static void RemoveByFile(string filePath) - { - Prefabs.RemoveByFile(filePath); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 150116a25..93fe641e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -12,7 +12,7 @@ namespace Barotrauma public string Name => ToString(); - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } public float PendingAdditionStrength { get; set; } public float AdditionStrength { get; set; } @@ -21,7 +21,7 @@ namespace Barotrauma protected float _strength; - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public virtual float Strength { get { return _strength; } @@ -43,10 +43,10 @@ namespace Barotrauma private float _nonClampedStrength = -1; public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength; - [Serialize("", true), Editable] - public string Identifier { get; private set; } + [Serialize("", IsPropertySaveable.Yes), Editable] + public Identifier Identifier { get; private set; } - [Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)] public float Probability { get; set; } = 1.0f; public float DamagePerSecond; @@ -73,7 +73,7 @@ namespace Barotrauma Prefab = prefab; PendingAdditionStrength = Prefab.GrainBurst; _strength = strength; - Identifier = prefab?.Identifier; + Identifier = prefab.Identifier; foreach (var periodicEffect in prefab.PeriodicEffects) { @@ -269,14 +269,15 @@ namespace Barotrauma } } - public float GetResistance(AfflictionPrefab affliction) + public float GetResistance(Identifier afflictionId) { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } + var affliction = AfflictionPrefab.Prefabs[afflictionId]; AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (!currentEffect.ResistanceFor.Any(r => - r.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) || - r.Equals(affliction.AfflictionType, StringComparison.OrdinalIgnoreCase))) + r == affliction.Identifier || + r == affliction.AfflictionType)) { return 0.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 4d42e7aa4..9e74e40e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -26,7 +26,7 @@ namespace Barotrauma private readonly List huskInfection = new List(); - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public override float Strength { get { return _strength; } @@ -211,10 +211,10 @@ namespace Barotrauma } character.Enabled = false; - Entity.Spawner.AddToRemoveQueue(character); + Entity.Spawner.AddEntityToRemoveQueue(character); UnsubscribeFromDeathEvent(); - string huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); + Identifier huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); if (prefab == null) @@ -230,8 +230,8 @@ namespace Barotrauma if (huskCharacterInfo != null) { var bodyTint = GetBodyTint(); - huskCharacterInfo.SkinColor = - Color.Lerp(huskCharacterInfo.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f); + huskCharacterInfo.Head.SkinColor = + Color.Lerp(huskCharacterInfo.Head.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f); } var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true); @@ -306,7 +306,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public static List AttachHuskAppendage(Character character, string afflictionIdentifier, XElement appendageDefinition = null, Ragdoll ragdoll = null) + public static List AttachHuskAppendage(Character character, Identifier afflictionIdentifier, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) { var appendage = new List(); if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction)) @@ -314,26 +314,26 @@ namespace Barotrauma DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!"); return appendage; } - string nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction); - string huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction); + Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction); + Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction); CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); - if (huskPrefab?.XDocument == null) + if (huskPrefab?.ConfigElement == null) { DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!"); return appendage; } - var mainElement = huskPrefab.XDocument.Root.IsOverride() ? huskPrefab.XDocument.Root.FirstElement() : huskPrefab.XDocument.Root; + var mainElement = huskPrefab.ConfigElement; var element = appendageDefinition; if (element == null) { - element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier)); + element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == afflictionIdentifier); } if (element == null) { DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!"); return appendage; } - string pathToAppendage = element.GetAttributeString("path", string.Empty); + ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty; XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage); if (doc == null) { return appendage; } if (ragdoll == null) @@ -344,10 +344,12 @@ namespace Barotrauma { ragdoll.Flip(); } - var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); - foreach (var jointElement in doc.Root.Elements("joint")) + + var root = doc.Root.FromPackage(pathToAppendage.ContentPackage); + var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); + foreach (var jointElement in root.GetChildElements("joint")) { - if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement)) + if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams); Limb attachLimb = null; @@ -388,15 +390,15 @@ namespace Barotrauma return appendage; } - public static string GetHuskedSpeciesName(string speciesName, AfflictionPrefabHusk prefab) + public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab) { return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName); } - public static string GetNonHuskedSpeciesName(string huskedSpeciesName, AfflictionPrefabHusk prefab) + public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab) { - string nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag); - return huskedSpeciesName.ToLowerInvariant().Remove(nonTag); + Identifier nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag); + return huskedSpeciesName.Remove(nonTag); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 087627e9e..da10f3719 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -2,28 +2,29 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { - static class CPRSettings + class CPRSettings : Prefab { - public static string FilePath { get; private set; } - public static bool IsLoaded { get; private set; } - public static float ReviveChancePerSkill { get; private set; } - public static float ReviveChanceExponent { get; private set; } - public static float ReviveChanceMin { get; private set; } - public static float ReviveChanceMax { get; private set; } - public static float StabilizationPerSkill { get; private set; } - public static float StabilizationMin { get; private set; } - public static float StabilizationMax { get; private set; } - public static float DamageSkillThreshold { get; private set; } - public static float DamageSkillMultiplier { get; private set; } + public readonly static PrefabSelector Prefabs = new PrefabSelector(); + public static CPRSettings Active => Prefabs.ActivePrefab; - private static string insufficientSkillAfflictionIdentifier { get; set; } - public static AfflictionPrefab InsufficientSkillAffliction + public readonly float ReviveChancePerSkill; + public readonly float ReviveChanceExponent; + public readonly float ReviveChanceMin; + public readonly float ReviveChanceMax; + public readonly float StabilizationPerSkill; + public readonly float StabilizationMin; + public readonly float StabilizationMax; + public readonly float DamageSkillThreshold; + public readonly float DamageSkillMultiplier; + + private readonly string insufficientSkillAfflictionIdentifier; + public AfflictionPrefab InsufficientSkillAffliction { get { @@ -34,7 +35,7 @@ namespace Barotrauma } } - public static void Load(XElement element, string filePath) + public CPRSettings(XElement element, AfflictionsFile file) : base(file, file.Path.Value.ToIdentifier()) { ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f); ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f); @@ -49,33 +50,26 @@ namespace Barotrauma DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f); insufficientSkillAfflictionIdentifier = element.GetAttributeString("insufficientskillaffliction", ""); - - IsLoaded = true; - FilePath = filePath; } - public static void Unload() - { - IsLoaded = false; - FilePath = null; - } + public override void Dispose() { } } class AfflictionPrefabHusk : AfflictionPrefab { - public AfflictionPrefabHusk(XElement element, string filePath, Type type = null) : base(element, filePath, type) + public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type) { - HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant(); - if (HuskedSpeciesName == null) + HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty); + if (HuskedSpeciesName.IsEmpty) { DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); - HuskedSpeciesName = "[speciesname]husk"; + HuskedSpeciesName = "[speciesname]husk".ToIdentifier(); } - TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true); + TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); if (TargetSpecies.Length == 0) { DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); - TargetSpecies = new string[] { "human" }; + TargetSpecies = new Identifier[] { CharacterPrefab.HumanSpeciesName }; } var attachElement = element.GetChildElement("attachlimb"); if (attachElement != null) @@ -112,9 +106,9 @@ namespace Barotrauma public float ActiveThreshold, DormantThreshold, TransitionThreshold; public float TransformThresholdOnDeath; - public readonly string HuskedSpeciesName; - public readonly string[] TargetSpecies; - public const string Tag = "[speciesname]"; + public readonly Identifier HuskedSpeciesName; + public readonly Identifier[] TargetSpecies; + public static readonly Identifier Tag = "[speciesname]".ToIdentifier(); public readonly bool TransferBuffs; public readonly bool SendMessages; @@ -123,124 +117,122 @@ namespace Barotrauma public readonly bool ControlHusk; } - class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier + class AfflictionPrefab : PrefabWithUintIdentifier { public class Effect { //this effect is applied when the strength is within this range - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinVitalityDecrease { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxVitalityDecrease { get; private set; } //how much the strength of the affliction changes per second - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float StrengthChange { get; private set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool MultiplyByMaxVitality { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinScreenBlur { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxScreenBlur { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinScreenDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxScreenDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinRadialDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxRadialDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinChromaticAberration { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxChromaticAberration { get; private set; } - [Serialize("255,255,255,255", false)] + [Serialize("255,255,255,255", IsPropertySaveable.No)] public Color GrainColor { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinGrainStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxGrainStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float ScreenEffectFluctuationFrequency { get; private set; } - - [Serialize(1.0f, false)] + + [Serialize(1.0f, IsPropertySaveable.No)] public float MinAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MinBuffMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxBuffMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MinSpeedMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxSpeedMultiplier { get; private set; } - - [Serialize(1.0f, false)] + + [Serialize(1.0f, IsPropertySaveable.No)] public float MinSkillMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxSkillMultiplier { get; private set; } + + private readonly Identifier[] resistanceFor; + public IReadOnlyList ResistanceFor => resistanceFor; - private readonly string[] resistanceFor; - public IEnumerable ResistanceFor - { - get { return resistanceFor; } - } - - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinResistance { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxResistance { get; private set; } - [Serialize("", false)] - public string DialogFlag { get; private set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier DialogFlag { get; private set; } - [Serialize("", false)] - public string Tag { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("", IsPropertySaveable.No)] + public Identifier Tag { get; private set; } + + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MinFaceTint { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MaxFaceTint { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MinBodyTint { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MaxBodyTint { get; private set; } /// /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character /// - public string[] BlockTransformation { get; private set; } + public Identifier[] BlockTransformation { get; private set; } public readonly Dictionary AfflictionStatValues = new Dictionary(); public readonly HashSet AfflictionAbilityFlags = new HashSet(); @@ -248,14 +240,14 @@ namespace Barotrauma //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); - public Effect(XElement element, string parentDebugName) + public Effect(ContentXElement element, string parentDebugName) { SerializableProperty.DeserializeProperties(this, element); - resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true); - BlockTransformation = element.GetAttributeStringArray("blocktransformation", new string[0], convertToLowerInvariant: true); + resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty()); + BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty()); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -288,9 +280,9 @@ namespace Barotrauma public readonly List StatusEffects = new List(); public readonly float MinInterval, MaxInterval; - public PeriodicEffect(XElement element, string parentDebugName) + public PeriodicEffect(ContentXElement element, string parentDebugName) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); } @@ -307,48 +299,28 @@ namespace Barotrauma } } - public static AfflictionPrefab InternalDamage; - public static AfflictionPrefab ImpactDamage; - public static AfflictionPrefab Bleeding; - public static AfflictionPrefab Burn; - public static AfflictionPrefab OxygenLow; - public static AfflictionPrefab Bloodloss; - public static AfflictionPrefab Pressure; - public static AfflictionPrefab Stun; - public static AfflictionPrefab RadiationSickness; + public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; + public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"]; + public static AfflictionPrefab Bleeding => Prefabs["bleeding"]; + public static AfflictionPrefab Burn => Prefabs["burn"]; + public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; + public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; + public static AfflictionPrefab Pressure => Prefabs["pressure"]; + public static AfflictionPrefab Stun => Prefabs["stun"]; + public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; public static readonly PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } - public static IEnumerable List - { - get - { - foreach (var prefab in Prefabs) - { - yield return prefab; - } - } - } - - public string FilePath { get; private set; } - - /// - /// Unique identifier that's generated by hashing the prefab's string identifier. - /// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer. - /// - public uint UIntIdentifier { get; set; } + public static IEnumerable List => Prefabs; // Arbitrary string that is used to identify the type of the affliction. - public readonly string AfflictionType; + public readonly Identifier AfflictionType; + private readonly ContentXElement configElement; + //Does the affliction affect a specific limb or the whole character public readonly bool LimbSpecific; @@ -356,18 +328,14 @@ namespace Barotrauma //(e.g. mental health problems on head, lack of oxygen on torso...) public readonly LimbType IndicatorLimb; - public string Identifier { get; private set; } - public string OriginalName { get { return Identifier; } } - public ContentPackage ContentPackage { get; private set; } - - public readonly string Name, Description; - public readonly string TranslationOverride; + public readonly LocalizedString Name, Description; + public readonly Identifier TranslationIdentifier; public readonly bool IsBuff; public readonly bool HealableInMedicalClinic; public readonly float HealCostMultiplier; public readonly int BaseHealCost; - public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription; + public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; //how high the strength has to be for the affliction to take affect public readonly float ActivationThreshold = 0.0f; @@ -392,7 +360,7 @@ namespace Barotrauma public float DamageOverlayAlpha; //steam achievement given when the affliction is removed from the controlled character - public readonly string AchievementOnRemoved; + public readonly Identifier AchievementOnRemoved; public readonly Sprite Icon; public readonly Color[] IconColors; @@ -407,11 +375,9 @@ namespace Barotrauma public IList PeriodicEffects => periodicEffects; - private readonly string typeName; - private readonly ConstructorInfo constructor; - public IEnumerable> TreatmentSuitability + public IEnumerable> TreatmentSuitability { get { @@ -420,255 +386,32 @@ namespace Barotrauma float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType)); if (suitability > 0.0f) { - yield return new KeyValuePair(itemPrefab.Identifier, suitability); + yield return new KeyValuePair(itemPrefab.Identifier, suitability); } } } } - public static void LoadAll(IEnumerable files) + public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", "")) { - CPRSettings.Unload(); - InternalDamage = null; - ImpactDamage = null; - Bleeding = null; - Burn = null; - OxygenLow = null; - Bloodloss = null; - Pressure = null; - Stun = null; - RadiationSickness = null; -#if CLIENT - CharacterHealth.DamageOverlay?.Remove(); - CharacterHealth.DamageOverlay = null; - CharacterHealth.DamageOverlayFile = string.Empty; -#endif - var prevPrefabs = Prefabs.AllPrefabs.SelectMany(kvp => kvp.Value).ToList(); - foreach (var prefab in prevPrefabs) - { - prefab?.Dispose(); - } - System.Diagnostics.Debug.Assert(Prefabs.Count() == 0, "All previous AfflictionPrefabs were not removed in AfflictionPrefab.LoadAll"); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - - if (InternalDamage == null) { DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs."); } - if (Bleeding == null) { DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs."); } - if (Burn == null) { DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs."); } - if (OxygenLow == null) { DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs."); } - if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); } - if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); } - if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); } - if (RadiationSickness == null) { DebugConsole.ThrowError("Affliction \"RadiationSickness\" not defined in the affliction prefabs."); } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - if (doc.Root.IsOverride()) - { - DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one."); - } - - List<(AfflictionPrefab prefab, XElement element)> loadedAfflictions = new List<(AfflictionPrefab prefab, XElement element)>(); - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase) && - !elementName.Equals("damageoverlay", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the affliction '{elementName}' in file '{file.Path}'"); - continue; - } - if (Prefabs.ContainsKey(identifier)) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Duplicate affliction: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - } - string type = sourceElement.GetAttributeString("type", ""); - switch (sourceElement.Name.ToString().ToLowerInvariant()) - { - case "cprsettings": - type = "cprsettings"; - break; - case "damageoverlay": - type = "damageoverlay"; - break; - } - - AfflictionPrefab prefab = null; - switch (type) - { - case "damageoverlay": -#if CLIENT - if (CharacterHealth.DamageOverlay != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding damage overlay with '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': damage overlay already loaded. Add tags as the parent of the custom damage overlay sprite to allow overriding the vanilla one."); - break; - } - } - CharacterHealth.DamageOverlay?.Remove(); - CharacterHealth.DamageOverlay = new Sprite(element); - CharacterHealth.DamageOverlayFile = file.Path; -#endif - break; - case "bleeding": - prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(AfflictionBleeding)); - break; - case "huskinfection": - case "alieninfection": - prefab = new AfflictionPrefabHusk(sourceElement, file.Path, typeof(AfflictionHusk)); - break; - case "cprsettings": - if (CPRSettings.IsLoaded) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding the CPR settings with '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': CPR settings already loaded. Add tags as the parent of the custom CPRSettings to allow overriding the vanilla values."); - break; - } - } - CPRSettings.Load(sourceElement, file.Path); - break; - case "damage": - case "burn": - case "oxygenlow": - case "bloodloss": - case "stun": - case "pressure": - case "internaldamage": - prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(Affliction)) - { - ContentPackage = file.ContentPackage - }; - break; - default: - prefab = new AfflictionPrefab(sourceElement, file.Path) - { - ContentPackage = file.ContentPackage - }; - break; - } - switch (identifier) - { - case "internaldamage": - InternalDamage = prefab; - break; - case "blunttrauma": - ImpactDamage = prefab; - break; - case "bleeding": - Bleeding = prefab; - break; - case "burn": - Burn = prefab; - break; - case "oxygenlow": - OxygenLow = prefab; - break; - case "bloodloss": - Bloodloss = prefab; - break; - case "pressure": - Pressure = prefab; - break; - case "stun": - Stun = prefab; - break; - case "radiationsickness": - RadiationSickness = prefab; - break; - } - if (ImpactDamage == null) { ImpactDamage = InternalDamage; } - - if (prefab != null) - { - loadedAfflictions.Add((prefab, sourceElement)); - Prefabs.Add(prefab, isOverride); - prefab.CalculatePrefabUIntIdentifier(Prefabs); - } - } - - //load the effects after all the afflictions in the file have been instantiated - //otherwise afflictions can't inflict other afflictions that are defined at a later point in the file - foreach ((AfflictionPrefab prefab, XElement element) in loadedAfflictions) - { - prefab.LoadEffects(element); - } - } - - public static void RemoveByFile(string filePath) - { - if (CPRSettings.FilePath == filePath) { CPRSettings.Unload(); } -#if CLIENT - if (CharacterHealth.DamageOverlayFile == filePath) - { - CharacterHealth.DamageOverlay?.Remove(); - CharacterHealth.DamageOverlay = null; - } -#endif - - Prefabs.RemoveByFile(filePath); - } - - public AfflictionPrefab(XElement element, string filePath, Type type = null) - { - FilePath = filePath; - - typeName = type == null ? element.Name.ToString() : type.Name; - if (typeName == "InternalDamage" && type == null) - { - type = typeof(Affliction); - } - - Identifier = element.GetAttributeString("identifier", ""); - - AfflictionType = element.GetAttributeString("type", ""); - TranslationOverride = element.GetAttributeString("translationoverride", null); - string translationId = TranslationOverride ?? Identifier; - Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", ""); - Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); + configElement = element; + + AfflictionType = element.GetAttributeIdentifier("type", ""); + TranslationIdentifier = element.GetAttributeIdentifier("translationoverride", Identifier); + Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}").Fallback(element.GetAttributeString("name", "")); + Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}").Fallback(element.GetAttributeString("description", "")); IsBuff = element.GetAttributeBool("isbuff", false); HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", !IsBuff && - !AfflictionType.Equals("geneticmaterialbuff", StringComparison.OrdinalIgnoreCase) && - !AfflictionType.Equals("geneticmaterialdebuff", StringComparison.OrdinalIgnoreCase)); + AfflictionType != "geneticmaterialbuff" && + AfflictionType != "geneticmaterialdebuff"); HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); if (element.Attribute("nameidentifier") != null) { - Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty), returnNull: true) ?? Name; + Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name); } LimbSpecific = element.GetAttributeBool("limbspecific", false); @@ -687,7 +430,8 @@ namespace Barotrauma MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); - ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : 0.05f)); + ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", + Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold)); TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); @@ -695,14 +439,14 @@ namespace Barotrauma KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f); - CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + translationId, true) ?? element.GetAttributeString("causeofdeathdescription", ""); - SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); + CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); + SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); IconColors = element.GetAttributeColorArray("iconcolors", null); AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false); - AchievementOnRemoved = element.GetAttributeString("achievementonremoved", ""); + AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", ""); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -724,43 +468,42 @@ namespace Barotrauma } } - try - { - if (type == null) - { - type = Type.GetType("Barotrauma." + typeName, true, true); - if (type == null) - { - DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\"."); - return; - } - } - } - catch - { - DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\"."); - type = typeof(Affliction); - } - constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } - private void LoadEffects(XElement element) + public static void LoadAllEffects() { - foreach (XElement subElement in element.Elements()) + Prefabs.ForEach(p => p.LoadEffects()); + } + + public static void ClearAllEffects() + { + Prefabs.ForEach(p => p.ClearEffects()); + } + + public void LoadEffects() + { + ClearEffects(); + foreach (var subElement in configElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "effect": - effects.Add(new Effect(subElement, Name)); + effects.Add(new Effect(subElement, Name.Value)); break; case "periodiceffect": - periodicEffects.Add(new PeriodicEffect(subElement, Name)); + periodicEffects.Add(new PeriodicEffect(subElement, Name.Value)); break; } } } + public void ClearEffects() + { + effects.Clear(); + periodicEffects.Clear(); + } + #if CLIENT public void ReloadSoundsIfNeeded() { @@ -770,7 +513,7 @@ namespace Barotrauma { foreach (var sound in statusEffect.Sounds) { - if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + if (sound.Sound == null) { RoundSound.Reload(sound); } } } } @@ -780,7 +523,7 @@ namespace Barotrauma { foreach (var sound in statusEffect.Sounds) { - if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + if (sound.Sound == null) { RoundSound.Reload(sound); } } } } @@ -789,7 +532,7 @@ namespace Barotrauma public override string ToString() { - return "AfflictionPrefab (" + Name + ")"; + return $"AfflictionPrefab ({Name})"; } public Affliction Instantiate(float strength, Character source = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs index 64472ae4e..408545fa2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs @@ -41,7 +41,7 @@ namespace Barotrauma invertControlsToggleTimer = 5.0f; if (Rand.Range(0.0f, 1.0f) < 0.5f) { - characterHealth.ReduceAffliction(null, "invertcontrols", 100); + characterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 6d6dcac8f..ad88711ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -19,23 +19,23 @@ namespace Barotrauma public Rectangle HighlightArea; - public readonly string Name; + public readonly LocalizedString Name; //public readonly List Afflictions = new List(); - public readonly Dictionary VitalityMultipliers = new Dictionary(); - public readonly Dictionary VitalityTypeMultipliers = new Dictionary(); + public readonly Dictionary VitalityMultipliers = new Dictionary(); + public readonly Dictionary VitalityTypeMultipliers = new Dictionary(); public LimbHealth() { } - public LimbHealth(XElement element, CharacterHealth characterHealth) + public LimbHealth(ContentXElement element, CharacterHealth characterHealth) { string limbName = element.GetAttributeString("name", null) ?? "generic"; if (limbName != "generic") { Name = TextManager.Get("HealthLimbName." + limbName); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -53,16 +53,16 @@ namespace Barotrauma continue; } - string afflictionIdentifier = subElement.GetAttributeString("identifier", ""); - string afflictionType = subElement.GetAttributeString("type", ""); + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + Identifier afflictionType = subElement.GetAttributeIdentifier("type", ""); float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - if (!string.IsNullOrEmpty(afflictionIdentifier)) + if (!afflictionIdentifier.IsEmpty) { - VitalityMultipliers.Add(afflictionIdentifier.ToLowerInvariant(), multiplier); + VitalityMultipliers.Add(afflictionIdentifier, multiplier); } else { - VitalityTypeMultipliers.Add(afflictionType.ToLowerInvariant(), multiplier); + VitalityTypeMultipliers.Add(afflictionType, multiplier); } break; } @@ -219,7 +219,7 @@ namespace Barotrauma InitProjSpecific(null, character); } - public CharacterHealth(XElement element, Character character, XElement limbHealthElement = null) + public CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement = null) { this.Character = character; InitIrremovableAfflictions(); @@ -230,7 +230,7 @@ namespace Barotrauma limbHealths.Clear(); limbHealthElement ??= element; - foreach (XElement subElement in limbHealthElement.Elements()) + foreach (var subElement in limbHealthElement.Elements()) { if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; } limbHealths.Add(new LimbHealth(subElement, this)); @@ -255,7 +255,7 @@ namespace Barotrauma } } - partial void InitProjSpecific(XElement element, Character character); + partial void InitProjSpecific(ContentXElement element, Character character); public IReadOnlyCollection GetAllAfflictions() { @@ -282,10 +282,13 @@ namespace Barotrauma private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex]; private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false)); - public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) + public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) => + GetAffliction(identifier.ToIdentifier(), allowLimbAfflictions); + + public Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions = true) => GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions); - public Affliction GetAfflictionOfType(string afflictionType, bool allowLimbAfflictions = true) + public Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions = true) => GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions); private Affliction GetAffliction(Func predicate, bool allowLimbAfflictions = true) @@ -409,7 +412,7 @@ namespace Barotrauma foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; - resistance += affliction.GetResistance(afflictionPrefab); + resistance += affliction.GetResistance(afflictionPrefab.Identifier); } return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); } @@ -436,37 +439,58 @@ namespace Barotrauma } private readonly List matchingAfflictions = new List(); - public void ReduceAffliction(Limb targetLimb, string afflictionIdentifier, float amount, ActionType? treatmentAction = null) + + public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null) { matchingAfflictions.Clear(); + matchingAfflictions.AddRange(afflictions.Keys); - if (targetLimb == null) - { - matchingAfflictions.AddRange(afflictions.Keys); - } - else - { - foreach (KeyValuePair kvp in afflictions) - { - var affliction = kvp.Key; - if (kvp.Value == null) - { - matchingAfflictions.Add(affliction); - } - else if (limbHealths[targetLimb.HealthIndex] == kvp.Value) - { - matchingAfflictions.Add(affliction); - } - } - } + ReduceMatchingAfflictions(amount, treatmentAction); + } + + public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null) + { + if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + + matchingAfflictions.Clear(); + matchingAfflictions.AddRange(afflictions.Keys); + matchingAfflictions.RemoveAll(a => + a.Prefab.Identifier != affliction && + a.Prefab.AfflictionType != affliction); + + ReduceMatchingAfflictions(amount, treatmentAction); + } - if (!string.IsNullOrEmpty(afflictionIdentifier)) - { - matchingAfflictions.RemoveAll(a => - !a.Prefab.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase) && - !a.Prefab.AfflictionType.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase)); - } + private IEnumerable GetAfflictionsForLimb(Limb targetLimb) + => afflictions.Keys.Where(k => afflictions[k] == limbHealths[targetLimb.HealthIndex]); + + public void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction = null) + { + if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } + matchingAfflictions.Clear(); + matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); + + ReduceMatchingAfflictions(amount, treatmentAction); + } + + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null) + { + if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } + + matchingAfflictions.Clear(); + matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); + + matchingAfflictions.RemoveAll(a => + a.Prefab.Identifier != affliction && + a.Prefab.AfflictionType != affliction); + + ReduceMatchingAfflictions(amount, treatmentAction); + } + + private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction) + { if (matchingAfflictions.Count == 0) { return; } float reduceAmount = amount / matchingAfflictions.Count; @@ -635,7 +659,7 @@ namespace Barotrauma if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { - if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase))) + if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } @@ -953,7 +977,7 @@ namespace Barotrauma /// A dictionary where the key is the identifier of the item and the value the suitability /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. /// If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds. - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) + public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) { //key = item identifier //float = suitability @@ -979,7 +1003,7 @@ namespace Barotrauma if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } if (ignoreHiddenAfflictions && strength < affliction.Prefab.ShowIconThreshold) { continue; } - foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { if (!treatmentSuitability.ContainsKey(treatment.Key)) { @@ -996,23 +1020,23 @@ namespace Barotrauma //normalize the suitabilities to a range of 0 to 1 if (normalize) { - foreach (string treatment in treatmentSuitability.Keys.ToList()) + foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) { treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); } } } - public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); + public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); - private readonly HashSet afflictionTags = new HashSet(); - public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) + private readonly HashSet afflictionTags = new HashSet(); + public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) { afflictionTags.Clear(); foreach (Affliction affliction in afflictions) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.Tag)) + if (currentEffect != null && !currentEffect.Tag.IsEmpty) { afflictionTags.Add(currentEffect.Tag); } @@ -1036,10 +1060,10 @@ namespace Barotrauma } foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction) { - if (statusEffectAffliction.affliction.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) || - statusEffectAffliction.affliction.Equals(affliction.Prefab.AfflictionType, StringComparison.OrdinalIgnoreCase)) + if (statusEffectAffliction.AfflictionIdentifier == affliction.Identifier || + statusEffectAffliction.AfflictionIdentifier == affliction.Prefab.AfflictionType) { - strength -= statusEffectAffliction.amount * statusEffectDuration; + strength -= statusEffectAffliction.ReduceAmount * statusEffectDuration; } } } @@ -1064,7 +1088,7 @@ namespace Barotrauma msg.Write((byte)activeAfflictions.Count); foreach (Affliction affliction in activeAfflictions) { - msg.Write(affliction.Prefab.UIntIdentifier); + msg.Write(affliction.Prefab.UintIdentifier); msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); @@ -1089,7 +1113,7 @@ namespace Barotrauma foreach (var (limbHealth, affliction) in limbAfflictions) { msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1); - msg.Write(affliction.Prefab.UIntIdentifier); + msg.Write(affliction.Prefab.UintIdentifier); msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); @@ -1144,7 +1168,7 @@ namespace Barotrauma public void Load(XElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index b40c60233..07f16e007 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -2,6 +2,7 @@ using System; using System.Xml.Linq; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma @@ -10,23 +11,23 @@ namespace Barotrauma { public string Name => "Damage Modifier"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - [Serialize(1.0f, false), Editable(DecimalCount = 2)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(DecimalCount = 2)] public float DamageMultiplier { get; private set; } - [Serialize(1.0f, false), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)] public float ProbabilityMultiplier { get; private set; } - [Serialize("0.0,360", false), Editable] + [Serialize("0.0,360", IsPropertySaveable.No), Editable] public Vector2 ArmorSector { get; @@ -35,14 +36,14 @@ namespace Barotrauma public Vector2 ArmorSectorInRadians => new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y)); - [Serialize(false, false), Editable] + [Serialize(false, IsPropertySaveable.No), Editable] public bool DeflectProjectiles { get; private set; } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string AfflictionIdentifiers { get @@ -56,7 +57,7 @@ namespace Barotrauma } } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string AfflictionTypes { get @@ -72,22 +73,11 @@ namespace Barotrauma private string rawAfflictionIdentifierString; private string rawAfflictionTypeString; - private string[] parsedAfflictionIdentifiers; - private string[] parsedAfflictionTypes; - public string[] ParsedAfflictionIdentifiers - { - get - { - return parsedAfflictionIdentifiers; - } - } - public string[] ParsedAfflictionTypes - { - get - { - return parsedAfflictionTypes; - } - } + private ImmutableArray parsedAfflictionIdentifiers; + private ImmutableArray parsedAfflictionTypes; + public ref readonly ImmutableArray ParsedAfflictionIdentifiers => ref parsedAfflictionIdentifiers; + + public ref readonly ImmutableArray ParsedAfflictionTypes => ref parsedAfflictionTypes; public DamageModifier(XElement element, string parentDebugName) { @@ -102,55 +92,58 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(rawAfflictionTypeString)) { - parsedAfflictionTypes = new string[0]; + parsedAfflictionTypes = Enumerable.Empty().ToImmutableArray(); return; } - string[] splitValue = rawAfflictionTypeString.Split(',', ','); - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].ToLowerInvariant().Trim(); - } - parsedAfflictionTypes = splitValue; + + parsedAfflictionTypes = rawAfflictionTypeString.Split(',', ',') + .Select(s => s.Trim()).ToIdentifiers().ToImmutableArray(); } private void ParseAfflictionIdentifiers() { if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString)) { - parsedAfflictionIdentifiers = new string[0]; + parsedAfflictionIdentifiers = Enumerable.Empty().ToImmutableArray(); return; } - string[] splitValue = rawAfflictionIdentifierString.Split(',', ','); - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].ToLowerInvariant().Trim(); - } - parsedAfflictionIdentifiers = splitValue; + + parsedAfflictionIdentifiers = rawAfflictionIdentifierString.Split(',', ',') + .Select(s => s.Trim()).ToIdentifiers().ToImmutableArray(); } - public bool MatchesAfflictionIdentifier(string identifier) + public bool MatchesAfflictionIdentifier(string identifier) => + MatchesAfflictionIdentifier(identifier.ToIdentifier()); + + public bool MatchesAfflictionIdentifier(Identifier identifier) { //if no identifiers have been defined, the damage modifier affects all afflictions if (AfflictionIdentifiers.Length == 0) { return true; } - return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + return parsedAfflictionIdentifiers.Any(id => id == identifier); } - public bool MatchesAfflictionType(string type) + public bool MatchesAfflictionType(string type) => + MatchesAfflictionType(type.ToIdentifier()); + + public bool MatchesAfflictionType(Identifier type) { //if no types have been defined, the damage modifier affects all afflictions if (AfflictionTypes.Length == 0) { return true; } - return parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase)); + return parsedAfflictionTypes.Any(t => t == type); } /// /// Returns true if the type or the identifier matches the defined types/identifiers. /// - public bool MatchesAffliction(string identifier, string type) + public bool MatchesAffliction(string identifier, string type) => + MatchesAffliction(identifier.ToIdentifier(), type.ToIdentifier()); + + public bool MatchesAffliction(Identifier identifier, Identifier type) { //if no identifiers or types have been defined, the damage modifier affects all afflictions if (AfflictionIdentifiers.Length == 0 && AfflictionTypes.Length == 0) { return true; } - return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase)) - || parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase)); + return parsedAfflictionIdentifiers.Any(id => id == identifier) + || parsedAfflictionTypes.Any(t => t == type); } public bool MatchesAffliction(Affliction affliction) => MatchesAffliction(affliction.Identifier, affliction.Prefab.AfflictionType); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 47a66ff4f..5f9372f84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using System; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -6,32 +7,29 @@ using System.Xml.Linq; namespace Barotrauma { - class HumanPrefab + class HumanPrefab : PrefabWithUintIdentifier { - [Serialize("notfound", false)] - public string Identifier { get; protected set; } - - [Serialize("any", false)] + [Serialize("any", IsPropertySaveable.No)] public string Job { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float Commonness { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float HealthMultiplier { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float HealthMultiplierInMultiplayer { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AimSpeed { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AimAccuracy { get; protected set; } - private readonly HashSet moduleFlags = new HashSet(); + private readonly HashSet moduleFlags = new HashSet(); - [Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")] + [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the NPC prefer to spawn in.")] public string ModuleFlags { get => string.Join(",", moduleFlags); @@ -43,16 +41,16 @@ namespace Barotrauma string[] splitFlags = value.Split(','); foreach (var f in splitFlags) { - moduleFlags.Add(f); + moduleFlags.Add(f.ToIdentifier()); } } } } - private readonly HashSet spawnPointTags = new HashSet(); + private readonly HashSet spawnPointTags = new HashSet(); - [Serialize("", true, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")] + [Serialize("", IsPropertySaveable.Yes, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")] public string SpawnPointTags { get => string.Join(",", spawnPointTags); @@ -64,27 +62,22 @@ namespace Barotrauma string[] splitTags = value.Split(','); foreach (var tag in splitTags) { - spawnPointTags.Add(tag.ToLowerInvariant()); + spawnPointTags.Add(tag.ToIdentifier()); } } } } - [Serialize(CampaignMode.InteractionType.None, false)] + [Serialize(CampaignMode.InteractionType.None, IsPropertySaveable.No)] public CampaignMode.InteractionType CampaignInteractionType { get; protected set; } - [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] + [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] public AIObjectiveIdle.BehaviorType Behavior { get; protected set; } - [Serialize(float.PositiveInfinity, false)] + [Serialize(float.PositiveInfinity, IsPropertySaveable.No)] public float ReportRange { get; protected set; } - public List PreferredOutpostModuleTypes { get; protected set; } - - public string OriginalName { get { return Identifier; } } - - - public string FilePath { get; protected set; } + public Identifier[] PreferredOutpostModuleTypes { get; protected set; } public XElement Element { get; protected set; } @@ -92,24 +85,22 @@ namespace Barotrauma public readonly Dictionary ItemSets = new Dictionary(); public readonly Dictionary CustomNPCSets = new Dictionary(); - public HumanPrefab(XElement element, string filePath) + public HumanPrefab(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - FilePath = filePath; SerializableProperty.DeserializeProperties(this, element); - Identifier = Identifier.ToLowerInvariant(); Job = Job.ToLowerInvariant(); Element = element; element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1))); element.GetChildElements("character").ForEach(e => CustomNPCSets.Add(e, e.GetAttributeFloat("commonness", 1))); - PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList(); + PreferredOutpostModuleTypes = element.GetAttributeIdentifierArray("preferredoutpostmoduletypes", Array.Empty()); } - public IEnumerable GetModuleFlags() + public IEnumerable GetModuleFlags() { return moduleFlags; } - public IEnumerable GetSpawnPointTags() + public IEnumerable GetSpawnPointTags() { return spawnPointTags; } @@ -139,7 +130,7 @@ namespace Barotrauma else { idleObjective.Behavior = Behavior; - foreach (string moduleType in PreferredOutpostModuleTypes) + foreach (Identifier moduleType in PreferredOutpostModuleTypes) { idleObjective.PreferredOutpostModuleTypes.Add(moduleType); } @@ -180,7 +171,7 @@ namespace Barotrauma { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; if (itemPrefab == null) { DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); @@ -217,28 +208,21 @@ namespace Barotrauma { character.Inventory.TryPutItem(item, null, item.AllowedSlots); } - if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck") + IdCard idCardComponent = item.GetComponent(); + if (idCardComponent != null) { - item.AddTag("name:" + character.Name); - var job = character.Info?.Job; - if (job != null) - { - item.AddTag("job:" + job.Name); - } - - IdCard idCardComponent = item.GetComponent(); - idCardComponent?.Initialize(character.Info); + idCardComponent.Initialize(null, character); if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) { idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; } - var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]); + var idCardTags = itemElement.GetAttributeStringArray("tags", Array.Empty()); foreach (string tag in idCardTags) { item.AddTag(tag); } - } + } foreach (WifiComponent wifiComponent in item.GetComponents()) { @@ -250,5 +234,7 @@ namespace Barotrauma InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 7fd0524ed..3f1993a7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -9,87 +9,87 @@ namespace Barotrauma { private readonly JobPrefab prefab; - private readonly Dictionary skills; + private readonly Dictionary skills; - public string Name - { - get { return prefab.Name; } - } + public LocalizedString Name => prefab.Name; - public string Description - { - get { return prefab.Description; } - } + public LocalizedString Description => prefab.Description; - public JobPrefab Prefab - { - get { return prefab; } - } - - public List Skills - { - get { return skills.Values.ToList(); } - } + public JobPrefab Prefab => prefab; + + public List Skills => skills.Values.ToList(); public int Variant; public Skill PrimarySkill { get; } - public Job(JobPrefab jobPrefab, Rand.RandSync randSync = Rand.RandSync.Unsynced, int variant = 0) + public Job(JobPrefab jobPrefab) : this(jobPrefab, randSync: Rand.RandSync.Unsynced, variant: 0) { } + + public Job(JobPrefab jobPrefab, Rand.RandSync randSync, int variant, params Skill[] s) { prefab = jobPrefab; Variant = variant; - skills = new Dictionary(); + skills = new Dictionary(); + foreach (var skill in s) { skills.Add(skill.Identifier, skill); } foreach (SkillPrefab skillPrefab in prefab.Skills) { - var skill = new Skill(skillPrefab, randSync); - skills.Add(skillPrefab.Identifier, skill); + Skill skill; + if (skills.ContainsKey(skillPrefab.Identifier)) + { + skill = skills[skillPrefab.Identifier]; + skills[skillPrefab.Identifier] = new Skill(skill.Identifier, skill.Level); + } + else + { + skill = new Skill(skillPrefab, randSync); + skills.Add(skillPrefab.Identifier, skill); + } if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; } } } public Job(XElement element) { - string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant(); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); JobPrefab p; if (!JobPrefab.Prefabs.ContainsKey(identifier)) { DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job."); - p = JobPrefab.Random(); + p = JobPrefab.Random(Rand.RandSync.Unsynced); } else { p = JobPrefab.Prefabs[identifier]; } prefab = p; - skills = new Dictionary(); - foreach (XElement subElement in element.Elements()) + skills = new Dictionary(); + foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; } - string skillIdentifier = subElement.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(skillIdentifier)) { continue; } + if (subElement.NameAsIdentifier() != "skill") { continue; } + Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + if (skillIdentifier.IsEmpty) { continue; } var skill = new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)); skills.Add(skillIdentifier, skill); if (skillIdentifier == prefab.PrimarySkill?.Identifier) { PrimarySkill = skill; } } } - public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static Job Random(Rand.RandSync randSync) { var prefab = JobPrefab.Random(randSync); var variant = Rand.Range(0, prefab.Variants, randSync); return new Job(prefab, randSync, variant); } - public float GetSkillLevel(string skillIdentifier) + public float GetSkillLevel(Identifier skillIdentifier) { - if (string.IsNullOrWhiteSpace(skillIdentifier)) { return 0.0f; } + if (skillIdentifier.IsEmpty) { return 0.0f; } skills.TryGetValue(skillIdentifier, out Skill skill); - return (skill == null) ? 0.0f : skill.Level; + return skill?.Level ?? 0.0f; } - public void IncreaseSkillLevel(string skillIdentifier, float increase, bool increasePastMax) + public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool increasePastMax) { if (skills.TryGetValue(skillIdentifier, out Skill skill)) { @@ -130,7 +130,7 @@ namespace Barotrauma else { string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; if (itemPrefab == null) { DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); @@ -192,27 +192,16 @@ namespace Barotrauma if (item.Prefab.Identifier == "idcard") { - if (spawnPoint != null) - { - foreach (string s in spawnPoint.IdCardTags) - { - item.AddTag(s); - if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) { item.Description = spawnPoint.IdCardDesc; } - } - } - item.AddTag("name:" + character.Name); - item.AddTag("job:" + Name); - IdCard idCardComponent = item.GetComponent(); - idCardComponent?.Initialize(character.Info); + idCardComponent?.Initialize(spawnPoint, character); } foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = character.TeamID; } - - if (parentItem != null) parentItem.Combine(item, user: null); + + if (parentItem != null) { parentItem.Combine(item, user: null); } foreach (XElement childItemElement in itemElement.Elements()) { @@ -227,7 +216,7 @@ namespace Barotrauma jobElement.Add(new XAttribute("name", Name)); jobElement.Add(new XAttribute("identifier", prefab.Identifier)); - foreach (KeyValuePair skill in skills) + foreach (KeyValuePair skill in skills) { jobElement.Add(new XElement("skill", new XAttribute("identifier", skill.Value.Identifier), new XAttribute("level", skill.Value.Level))); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 442f29ab9..1d1c112f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -9,54 +10,78 @@ namespace Barotrauma { public class AutonomousObjective { - public string identifier; - public string option; - public readonly float priorityModifier; - public readonly bool ignoreAtOutpost; + public readonly Identifier Identifier; + public readonly Identifier Option; + public readonly float PriorityModifier; + public readonly bool IgnoreAtOutpost; public AutonomousObjective(XElement element) { - identifier = element.GetAttributeString("identifier", null); + Identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); //backwards compatibility - if (string.IsNullOrEmpty(identifier)) + if (Identifier == Identifier.Empty) { - identifier = element.GetAttributeString("aitag", null); + Identifier = element.GetAttributeIdentifier("aitag", Identifier.Empty); } - option = element.GetAttributeString("option", null); - priorityModifier = element.GetAttributeFloat("prioritymodifier", 1); - priorityModifier = MathHelper.Max(priorityModifier, 0); - ignoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false); + Option = element.GetAttributeIdentifier("option", Identifier.Empty); + PriorityModifier = element.GetAttributeFloat("prioritymodifier", 1); + PriorityModifier = MathHelper.Max(PriorityModifier, 0); + IgnoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false); } } - partial class JobPrefab : IPrefab, IDisposable + class ItemRepairPriority : Prefab + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public readonly float Priority; + + public ItemRepairPriority(XElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("tag", Identifier.Empty)) + { + Priority = element.GetAttributeFloat("priority", -1f); + if (Priority < 0) + { + DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}."); + } + } + + public override void Dispose() { } + } + + class JobVariant + { + public JobPrefab Prefab; + public int Variant; + public JobVariant(JobPrefab prefab, int variant) + { + Prefab = prefab; + Variant = variant; + } + } + + partial class JobPrefab : PrefabWithUintIdentifier { public static readonly PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; Prefabs.Remove(this); } - private static readonly Dictionary _itemRepairPriorities = new Dictionary(); + private static readonly Dictionary _itemRepairPriorities = new Dictionary(); /// /// Tag -> priority. /// - public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; + public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; - public static XElement NoJobElement; + public static ContentXElement NoJobElement; public static JobPrefab Get(string identifier) { - if (Prefabs == null) - { - DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded."); - return null; - } if (Prefabs.ContainsKey(identifier)) { return Prefabs[identifier]; @@ -70,62 +95,41 @@ namespace Barotrauma public class PreviewItem { - public readonly string ItemIdentifier; + public readonly Identifier ItemIdentifier; public readonly bool ShowPreview; - public PreviewItem(string itemIdentifier, bool showPreview) + public PreviewItem(Identifier itemIdentifier, bool showPreview) { ItemIdentifier = itemIdentifier; ShowPreview = showPreview; } } - public readonly Dictionary ItemSets = new Dictionary(); - public readonly Dictionary> PreviewItems = new Dictionary>(); + public readonly Dictionary ItemSets = new Dictionary(); + public readonly ImmutableDictionary> PreviewItems; public readonly List Skills = new List(); public readonly List AutonomousObjectives = new List(); - public readonly List AppropriateOrders = new List(); + public readonly List AppropriateOrders = new List(); - [Serialize("1,1,1,1", false)] + [Serialize("1,1,1,1", IsPropertySaveable.No)] public Color UIColor { get; private set; } - [Serialize("notfound", false)] - public string Identifier - { - get; - private set; - } + public readonly LocalizedString Name; - [Serialize("notfound", false)] - public string Name - { - get; - private set; - } - - [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] + [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] public AIObjectiveIdle.BehaviorType IdleBehavior { get; private set; } - public string OriginalName { get { return Identifier; } } + public readonly LocalizedString Description; - public ContentPackage ContentPackage { get; private set; } - - [Serialize("", false)] - public string Description - { - get; - private set; - } - - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool OnlyJobSpecificDialog { get; @@ -133,7 +137,7 @@ namespace Barotrauma } //the number of these characters in the crew the player starts with in the single player campaign - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int InitialCount { get; @@ -141,7 +145,7 @@ namespace Barotrauma } //if set to true, a client that has chosen this as their preferred job will get it no matter what - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool AllowAlways { get; @@ -149,7 +153,7 @@ namespace Barotrauma } //how many crew members can have the job (only one captain etc) - [Serialize(100, false)] + [Serialize(100, IsPropertySaveable.No)] public int MaxNumber { get; @@ -158,21 +162,21 @@ namespace Barotrauma //how many crew members are REQUIRED to have the job //(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference) - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int MinNumber { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinKarma { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float PriceMultiplier { get; @@ -180,7 +184,7 @@ namespace Barotrauma } // TODO: not used - [Serialize(10.0f, false)] + [Serialize(10.0f, IsPropertySaveable.No)] public float Commonness { get; @@ -188,7 +192,7 @@ namespace Barotrauma } //how much the vitality of the character is increased/reduced from the default value - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float VitalityModifier { get; @@ -196,7 +200,7 @@ namespace Barotrauma } //whether the job should be available to NPCs - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool HiddenJob { get; @@ -208,35 +212,33 @@ namespace Barotrauma public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill); - public string FilePath { get; private set; } - - public XElement Element { get; private set; } - public XElement ClothingElement { get; private set; } + public ContentXElement Element { get; private set; } + public ContentXElement ClothingElement { get; private set; } public int Variants { get; private set; } - public JobPrefab(XElement element, string filePath) + public JobPrefab(ContentXElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - FilePath = filePath; SerializableProperty.DeserializeProperties(this, element); Name = TextManager.Get("JobName." + Identifier); - Description = TextManager.Get("JobDescription." + Identifier, returnNull: true) ?? string.Empty; - Identifier = Identifier.ToLowerInvariant(); + Description = TextManager.Get("JobDescription." + Identifier); Element = element; + var previewItems = new Dictionary>(); + int variant = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "itemset": ItemSets.Add(variant, subElement); - PreviewItems[variant] = new List(); + previewItems[variant] = new List(); loadItemIdentifiers(subElement, variant); variant++; break; case "skills": - foreach (XElement skillElement in subElement.Elements()) + foreach (var skillElement in subElement.Elements()) { Skills.Add(new SkillPrefab(skillElement)); } @@ -246,7 +248,7 @@ namespace Barotrauma break; case "appropriateobjectives": case "appropriateorders": - subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeString("identifier", "").ToLowerInvariant())); + subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeIdentifier("identifier", ""))); break; case "jobicon": Icon = new Sprite(subElement.FirstElement()); @@ -267,19 +269,22 @@ namespace Barotrauma continue; } - string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - if (string.IsNullOrWhiteSpace(itemIdentifier)) + Identifier itemIdentifier = itemElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (itemIdentifier.IsEmpty) { DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier."); } else { - PreviewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true))); + previewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true))); } loadItemIdentifiers(itemElement, variant); } } + PreviewItems = previewItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())) + .ToImmutableDictionary(); + Variants = variant; Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start)); @@ -287,77 +292,7 @@ namespace Barotrauma // Disabled on purpose, TODO: remove all references? //ClothingElement = element.GetChildElement("PortraitClothing"); } - - public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => !p.HiddenJob, sync); - - public static void LoadAll(IEnumerable files) - { - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - if (doc.Root.IsOverride()) - { - DebugConsole.ThrowError($"Error in '{file.Path}': Cannot override all job prefabs, because many of them are required by the main game! Please try overriding jobs one by one."); - } - foreach (XElement element in mainElement.Elements()) - { - if (element.IsOverride()) - { - var job = new JobPrefab(element.FirstElement(), file.Path) - { - ContentPackage = file.ContentPackage - }; - Prefabs.Add(job, true); - } - else - { - if (!element.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) { continue; } - var job = new JobPrefab(element, file.Path) - { - ContentPackage = file.ContentPackage - }; - Prefabs.Add(job, false); - } - } - NoJobElement ??= mainElement.GetChildElement("nojob"); - var itemRepairPrioritiesElement = mainElement.GetChildElement("ItemRepairPriorities"); - if (itemRepairPrioritiesElement != null) - { - foreach (var subElement in itemRepairPrioritiesElement.Elements()) - { - string tag = subElement.GetAttributeString("tag", null); - if (tag != null) - { - float priority = subElement.GetAttributeFloat("priority", -1f); - if (priority >= 0) - { - _itemRepairPriorities.TryAdd(tag, priority); - } - else - { - DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}."); - } - } - else - { - DebugConsole.AddWarning($"The 'tag' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}."); - } - } - } - } - - public static void RemoveByFile(string filePath) - { - Prefabs.RemoveByFile(filePath); - } + public static JobPrefab Random(Rand.RandSync sync) => Prefabs.GetRandom(p => !p.HiddenJob, sync); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index 2baae954b..41a894960 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -4,12 +4,12 @@ namespace Barotrauma { class Skill { - private float level; - - public string Identifier { get; } + public readonly Identifier Identifier; public const float MaximumSkill = 100.0f; + private float level; + public float Level { get { return level; } @@ -21,18 +21,11 @@ namespace Barotrauma level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); } - private Sprite icon; - public Sprite Icon - { - get - { - if (icon == null) - { - icon = GetIcon(); - } - return icon; - } - } + private Identifier iconJobId; + + public Sprite Icon => !iconJobId.IsEmpty && JobPrefab.Prefabs.TryGet(iconJobId, out var jobPrefab) + ? jobPrefab.Icon + : null; public readonly float PriceMultiplier = 1.0f; @@ -40,39 +33,42 @@ namespace Barotrauma { Identifier = prefab.Identifier; level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync); - icon = GetIcon(); + iconJobId = GetIconJobId(); PriceMultiplier = prefab.PriceMultiplier; } - public Skill(string identifier, float level) + public Skill(Identifier identifier, float level) { Identifier = identifier; this.level = level; - icon = GetIcon(); + iconJobId = GetIconJobId(); } - private Sprite GetIcon() + private Identifier GetIconJobId() { - string jobId = null; - switch (Identifier.ToLowerInvariant()) + Identifier jobId = Identifier.Empty; + if (Identifier == "electrical") { - case "electrical": - jobId = "engineer"; - break; - case "helm": - jobId = "captain"; - break; - case "mechanical": - jobId = "mechanic"; - break; - case "medical": - jobId = "medicaldoctor"; - break; - case "weapons": - jobId = "securityofficer"; - break; + jobId = "engineer".ToIdentifier(); } - return jobId != null && JobPrefab.Prefabs.ContainsKey(jobId) ? JobPrefab.Prefabs[jobId].IconSmall : null; + else if (Identifier == "helm") + { + jobId = "captain".ToIdentifier(); + } + else if (Identifier == "mechanical") + { + jobId = "mechanic".ToIdentifier(); + } + else if (Identifier == "medical") + { + jobId = "medicaldoctor".ToIdentifier(); + } + else if (Identifier == "weapons") + { + jobId = "securityofficer".ToIdentifier(); + } + + return jobId; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs index d4bb305f3..3a8ed2a6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs @@ -5,7 +5,7 @@ namespace Barotrauma { class SkillPrefab { - public readonly string Identifier; + public readonly Identifier Identifier; public Range LevelRange { get; private set; } @@ -16,9 +16,9 @@ namespace Barotrauma public bool IsPrimarySkill { get; } - public SkillPrefab(XElement element) + public SkillPrefab(ContentXElement element) { - Identifier = element.GetAttributeString("identifier", ""); + Identifier = element.GetAttributeIdentifier("identifier", ""); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f); var levelString = element.GetAttributeString("level", ""); if (levelString.Contains(",")) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 978410599..97e51110f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -576,7 +576,7 @@ namespace Barotrauma } } - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -622,7 +622,7 @@ namespace Barotrauma body.BodyType = BodyType.Dynamic; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -644,9 +644,10 @@ namespace Barotrauma } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } - if (character.VariantOf != null && character.Params.VariantFile != null) + if (!character.VariantOf.IsEmpty) { - var attackElement = character.Params.VariantFile.Root.GetChildElement("attack"); + var attackElement = CharacterPrefab.Prefabs.TryGet(character.VariantOf, out var basePrefab) + ? basePrefab.ConfigElement.GetChildElement("attack") : null; if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); @@ -668,7 +669,7 @@ namespace Barotrauma InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public void MoveToPos(Vector2 pos, float force, bool pullFromCenter = false) { @@ -1084,7 +1085,7 @@ namespace Barotrauma attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this); } } - /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) + /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)) { // TODO: use the hit pos? var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs index d2465d2f1..e12965df7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs @@ -8,15 +8,7 @@ namespace Barotrauma { class NPCPersonalityTrait { - private static List list = new List(); - public static List List - { - get { return list; } - } - - public readonly string FilePath; - - public readonly string Name; + public readonly Identifier Name; public readonly List AllowedDialogTags; @@ -26,20 +18,32 @@ namespace Barotrauma get { return commonness; } } - public NPCPersonalityTrait(XElement element, string filePath) + public static IEnumerable GetAll(LanguageIdentifier language) { - FilePath = filePath; - Name = element.GetAttributeString("name", ""); - AllowedDialogTags = new List(element.GetAttributeStringArray("alloweddialogtags", new string[0])); - commonness = element.GetAttributeFloat("commonness", 1.0f); + return NPCConversationCollection.Collections[language] + .SelectMany(cc => cc.PersonalityTraits.Values); + } - list.Add(this); + public static NPCPersonalityTrait Get(LanguageIdentifier language, Identifier traitName) + { + return NPCConversationCollection.Collections[language] + .FirstOrDefault(cc => cc.PersonalityTraits.ContainsKey(traitName)) + .PersonalityTraits[traitName]; + } + + public NPCPersonalityTrait(XElement element) + { + Name = element.GetAttributeIdentifier("name", ""); + AllowedDialogTags = new List(element.GetAttributeStringArray("alloweddialogtags", Array.Empty())); + commonness = element.GetAttributeFloat("commonness", 1.0f); } public static NPCPersonalityTrait GetRandom(string seed) { + #warning TODO: implement NPCPersonality content type and revise this for determinism var rand = new MTRandom(ToolBox.StringToInt(seed)); - return ToolBox.SelectWeightedRandom(list, list.Select(t => t.commonness).ToList(), rand); + var list = GetAll(GameSettings.CurrentConfig.Language); + return ToolBox.SelectWeightedRandom(list, t => t.commonness, rand); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 7b004f09d..f6a9d1d4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -21,71 +21,72 @@ namespace Barotrauma abstract class GroundedMovementParams : AnimationParams { - [Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)] public Vector2 StepSize { get; set; } - [Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float HeadPosition { get; set; } - [Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float TorsoPosition { get; set; } - [Serialize(1f, true, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)] + [Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)] public float StepLiftHeadMultiplier { get; set; } - [Serialize(0f, true, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)] public float StepLiftAmount { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool MultiplyByDir { get; set; } - [Serialize(0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)] public float StepLiftOffset { get; set; } - [Serialize(2f, true, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)] + [Serialize(2f, IsPropertySaveable.Yes, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)] public float StepLiftFrequency { get; set; } - [Serialize(0.75f, true, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)] + [Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)] public float BackwardsMovementMultiplier { get; set; } } abstract class SwimParams : AnimationParams { - [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float SteerTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float LegTorque { get; set; } } abstract class AnimationParams : EditableParams, IMemorizable { - public string SpeciesName { get; private set; } + public Identifier SpeciesName { get; private set; } public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch; public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast; - protected static Dictionary> allAnimations = new Dictionary>(); + protected static Dictionary> allAnimations = new Dictionary>(); + /// allAnimations[speciesName][fileName] private float _movementSpeed; - [Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] public float MovementSpeed { get => _movementSpeed; set => _movementSpeed = value; } - [Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float CycleSpeed { get; set; } /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float HeadAngle { get => float.IsNaN(HeadAngleInRadians) ? float.NaN : MathHelper.ToDegrees(HeadAngleInRadians); @@ -102,7 +103,7 @@ namespace Barotrauma /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float TorsoAngle { get => float.IsNaN(TorsoAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TorsoAngleInRadians); @@ -117,49 +118,44 @@ namespace Barotrauma public float TorsoAngleInRadians { get; private set; } = float.NaN; - [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TorsoTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float FootTorque { get; set; } - [Serialize(AnimationType.NotDefined, true), Editable] + [Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable] public virtual AnimationType AnimationType { get; protected set; } - [Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ArmIKStrength { get; set; } - [Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandIKStrength { get; set; } - public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}"; - public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml"); + public static string GetDefaultFileName(Identifier speciesName, AnimationType animType) => $"{speciesName.Value.CapitaliseFirstInvariant()}{animType}"; + public static string GetDefaultFile(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml"); - public static string GetFolder(string speciesName) + public static string GetFolder(Identifier speciesName) { CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName); - if (prefab?.XDocument == null) + if (prefab?.ConfigElement == null) { DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'"); return string.Empty; } - return GetFolder(prefab.XDocument, prefab.FilePath); + return GetFolder(prefab.ConfigElement, prefab.FilePath.Value); } - public static string GetFolder(XDocument doc, string filePath) + private static string GetFolder(ContentXElement root, string filePath) { - var root = doc.Root; - if (root?.IsOverride() ?? false) - { - root = root.FirstElement(); - } - var folder = root?.Element("animations")?.GetAttributeString("folder", string.Empty); + var folder = root?.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value; if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { - folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations"); + folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations"); } return folder.CleanUpPathCrossPlatform(true); } @@ -167,9 +163,9 @@ namespace Barotrauma /// /// Selects a random filepath from multiple paths, matching the specified animation type. /// - public static string GetRandomFilePath(IEnumerable filePaths, AnimationType type) + public static string GetRandomFilePath(IReadOnlyList filePaths, AnimationType type) { - return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.Server); + return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.ServerAndClient); } /// @@ -194,11 +190,12 @@ namespace Barotrauma public static T GetDefaultAnimParams(Character character, AnimationType animType) where T : AnimationParams, new() { - string speciesName = character.VariantOf ?? character.SpeciesName; - if (character.VariantOf != null && character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeString("folder", null) != null) + Identifier speciesName = character.SpeciesName; + if (!character.VariantOf.IsEmpty + && (character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeStringUnrestricted("folder", null)).IsNullOrEmpty()) { - // Use the overridden animations defined in the variant definition file. - speciesName = character.SpeciesName; + // Use the base animations defined in the base definition file. + speciesName = character.VariantOf; } return GetAnimParams(speciesName, animType, GetDefaultFileName(speciesName, animType)); } @@ -207,7 +204,7 @@ namespace Barotrauma /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! /// If a custom folder is used, it's defined in the character info file. /// - public static T GetAnimParams(string speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new() + public static T GetAnimParams(Identifier speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new() { if (!allAnimations.TryGetValue(speciesName, out Dictionary anims)) { @@ -239,7 +236,7 @@ namespace Barotrauma } else { - selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + selectedFile = filteredFiles.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); @@ -257,10 +254,11 @@ namespace Barotrauma throw new Exception("[AnimationParams] Selected file null!"); } DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}."); + var characterPrefab = CharacterPrefab.Prefabs[speciesName]; T a = new T(); - if (a.Load(selectedFile, speciesName)) + if (a.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) { - fileName = Path.GetFileNameWithoutExtension(selectedFile); + fileName = IO.Path.GetFileNameWithoutExtension(selectedFile); if (!anims.ContainsKey(fileName)) { anims.Add(fileName, a); @@ -277,7 +275,7 @@ namespace Barotrauma public static void ClearCache() => allAnimations.Clear(); - public static AnimationParams Create(string fullPath, string speciesName, AnimationType animationType, Type type) + public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type type) { if (type == typeof(HumanWalkParams)) { @@ -317,7 +315,7 @@ namespace Barotrauma /// /// Note: Overrides old animations, if found! /// - public static T Create(string fullPath, string speciesName, AnimationType animationType) where T : AnimationParams, new() + public static T Create(string fullPath, Identifier speciesName, AnimationType animationType) where T : AnimationParams, new() { if (animationType == AnimationType.NotDefined) { @@ -328,7 +326,7 @@ namespace Barotrauma anims = new Dictionary(); allAnimations.Add(speciesName, anims); } - var fileName = Path.GetFileNameWithoutExtension(fullPath); + var fileName = IO.Path.GetFileNameWithoutExtension(fullPath); if (anims.ContainsKey(fileName)) { DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red); @@ -337,10 +335,12 @@ namespace Barotrauma var instance = new T(); XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString())); instance.doc = new XDocument(animationElement); - instance.UpdatePath(fullPath); + var characterPrefab = CharacterPrefab.Prefabs[speciesName]; + var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath); + instance.UpdatePath(contentPath); instance.IsLoaded = instance.Deserialize(animationElement); instance.Save(); - instance.Load(fullPath, speciesName); + instance.Load(contentPath, speciesName); anims.Add(fileName, instance); DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite); return instance; @@ -349,7 +349,7 @@ namespace Barotrauma public bool Serialize() => base.Serialize(); public bool Deserialize() => base.Deserialize(); - protected bool Load(string file, string speciesName) + protected bool Load(ContentPath file, Identifier speciesName) { if (Load(file)) { @@ -359,7 +359,7 @@ namespace Barotrauma return false; } - protected override void UpdatePath(string newPath) + protected override void UpdatePath(ContentPath newPath) { if (SpeciesName == null) { @@ -464,7 +464,8 @@ namespace Barotrauma var copy = new T { IsLoaded = true, - doc = new XDocument(doc) + doc = new XDocument(doc), + Path = Path }; copy.Deserialize(); copy.Serialize(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index c2257379b..e844e53a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -11,7 +11,7 @@ namespace Barotrauma } public static FishWalkParams GetAnimParams(Character character, string fileName = null) { - return Check(character) ? GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.Walk, fileName) : Empty; + return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName) : Empty; } protected static FishWalkParams Empty = new FishWalkParams(); @@ -27,7 +27,7 @@ namespace Barotrauma } public static FishRunParams GetAnimParams(Character character, string fileName = null) { - return Check(character) ? GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.Run, fileName) : Empty; + return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Run, fileName) : Empty; } protected static FishRunParams Empty = new FishRunParams(); @@ -40,7 +40,7 @@ namespace Barotrauma public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); public static FishSwimFastParams GetAnimParams(Character character, string fileName = null) { - return GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.SwimFast, fileName); + return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); } public override void StoreSnapshot() => StoreSnapshot(); @@ -51,7 +51,7 @@ namespace Barotrauma public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null) { - return GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.SwimSlow, fileName); + return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); } public override void StoreSnapshot() => StoreSnapshot(); @@ -69,35 +69,35 @@ namespace Barotrauma return true; } - [Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] public bool Flip { get; set; } - [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] public float FlipCooldown { get; set; } - [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] public float FlipDelay { get; set; } - [Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float HeadMoveForce { get; set; } - [Serialize(10.0f, true, description: "How much force is used to move the torso to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How much force is used to move the torso to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float TorsoMoveForce { get; set; } - [Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(8.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveForce { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TailTorque { get; set; } - [Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float LegTorque { get; set; } /// /// The angle of the collider when standing (i.e. out of water). /// In degrees. /// - [Serialize(0f, true, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] + [Serialize(0f, IsPropertySaveable.Yes, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] public float ColliderStandAngle { get => MathHelper.ToDegrees(ColliderStandAngleInRadians); @@ -105,7 +105,7 @@ namespace Barotrauma } public float ColliderStandAngleInRadians { get; private set; } - [Serialize(null, true), Editable] + [Serialize(null, IsPropertySaveable.Yes), Editable] public string FootAngles { get => ParseFootAngles(FootAnglesInRadians); @@ -120,7 +120,7 @@ namespace Barotrauma /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float TailAngle { get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians); @@ -137,41 +137,40 @@ namespace Barotrauma abstract class FishSwimParams : SwimParams, IFishAnimation { - [Serialize(false, true, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable] public bool UseSineMovement { get; set; } - [Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] public bool Flip { get; set; } - [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] public float FlipCooldown { get; set; } - [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] public float FlipDelay { get; set; } - [Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")] public bool Mirror { get; set; } - [Editable, Serialize(true, true, description: "Disabling this will make mirroring instantaneous.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Disabling this will make mirroring instantaneous.")] public bool MirrorLerp { get; set; } - [Serialize(5f, true), Editable] + [Serialize(5f, IsPropertySaveable.Yes), Editable] public float WaveAmplitude { get; set; } - [Serialize(10.0f, true), Editable] + [Serialize(10.0f, IsPropertySaveable.Yes), Editable] public float WaveLength { get; set; } - [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TailTorque { get; set; } - [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] + [Serialize(1f, IsPropertySaveable.Yes, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] public float TailTorqueMultiplier { get; set; } - - [Serialize(null, true), Editable] + [Serialize(null, IsPropertySaveable.Yes), Editable] public string FootAngles { get => ParseFootAngles(FootAnglesInRadians); @@ -186,7 +185,7 @@ namespace Barotrauma /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float TailAngle { get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 43ed47198..2fdb19b98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -26,13 +26,13 @@ namespace Barotrauma class HumanCrouchParams : HumanGroundedParams { - [Serialize(0.0f, true, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] public float MoveDownAmountWhenStationary { get; set; } - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float ExtraHeadAngleWhenStationary { get; set; } - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float ExtraTorsoAngleWhenStationary { get; set; } public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); @@ -69,25 +69,25 @@ namespace Barotrauma abstract class HumanSwimParams : SwimParams, IHumanAnimation { - [Serialize(0.5f, true), Editable(DecimalCount = 2)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float LegMoveAmount { get; set; } - [Serialize(5.0f, true), Editable] + [Serialize(5.0f, IsPropertySaveable.Yes), Editable] public float LegCycleLength { get; set; } - [Serialize("0.5, 0.1", true), Editable(DecimalCount = 2)] + [Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } - [Serialize(5.0f, true), Editable] + [Serialize(5.0f, IsPropertySaveable.Yes), Editable] public float HandCycleSpeed { get; set; } - [Serialize("0.0, 0.0", true), Editable(DecimalCount = 2)] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 HandMoveOffset { get; set; } /// /// In degrees. /// - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float FootAngle { get => MathHelper.ToDegrees(FootAngleInRadians); @@ -98,34 +98,34 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)] public float ArmMoveStrength { get; set; } - [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation { - [Serialize(0.3f, true, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] + [Serialize(0.3f, IsPropertySaveable.Yes, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float GetUpForce { get; set; } - [Serialize(0.25f, true, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)] public float HeadLeanAmount { get; set; } - [Serialize(0.25f, true, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)] public float TorsoLeanAmount { get; set; } - [Serialize(15.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(15.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveStrength { get; set; } - [Serialize(0f, true, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)] public float FootLiftHorizontalFactor { get; set; } /// /// In degrees. /// - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float FootAngle { get => MathHelper.ToDegrees(FootAngleInRadians); @@ -136,25 +136,25 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] public Vector2 FootMoveOffset { get; set; } - [Serialize(10.0f, true, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float LegBendTorque { get; set; } - [Serialize("0.4, 0.15", true, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)] + [Serialize("0.4, 0.15", IsPropertySaveable.Yes, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } - [Serialize("-0.15, 0.0", true, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)] + [Serialize("-0.15, 0.0", IsPropertySaveable.Yes, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)] public Vector2 HandMoveOffset { get; set; } - [Serialize(-1.0f, true, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)] + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)] public float HandClampY { get; set; } - [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ArmMoveStrength { get; set; } - [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 4d03cd6fd..5733c62f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -5,6 +5,7 @@ using System.Xml.Linq; using System.Xml; using System.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; #if CLIENT using SoundType = Barotrauma.CharacterSound.SoundType; #endif @@ -16,94 +17,94 @@ namespace Barotrauma /// class CharacterParams : EditableParams { - [Serialize("", true), Editable] - public string SpeciesName { get; private set; } + [Serialize("", IsPropertySaveable.Yes), Editable] + public Identifier SpeciesName { get; private set; } - [Serialize("", true, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] public string SpeciesTranslationOverride { get; private set; } - [Serialize("", true, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] public string DisplayName { get; private set; } - [Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] - public string Group { get; private set; } + [Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] + public Identifier Group { get; private set; } - [Serialize(false, true), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] public bool Humanoid { get; private set; } - [Serialize(false, true), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } - [Serialize(false, true, description: "Can the creature interact with items?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable] public bool CanInteract { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool Husk { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseHuskAppendage { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool NeedsAir { get; set; } - [Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } - [Serialize(false, false), Editable] + [Serialize(false, IsPropertySaveable.No), Editable] public bool CanSpeak { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseBossHealthBar { get; private set; } - [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] public float Noise { get; set; } - [Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)] public float Visibility { get; set; } - [Serialize("blood", true), Editable] + [Serialize("blood", IsPropertySaveable.Yes), Editable] public string BloodDecal { get; private set; } - [Serialize("blooddrop", true), Editable] + [Serialize("blooddrop", IsPropertySaveable.Yes), Editable] public string BleedParticleAir { get; private set; } - [Serialize("waterblood", true), Editable] + [Serialize("waterblood", IsPropertySaveable.Yes), Editable] public string BleedParticleWater { get; private set; } - [Serialize(1f, true), Editable] + [Serialize(1f, IsPropertySaveable.Yes), Editable] public float BleedParticleMultiplier { get; private set; } - - [Serialize(true, true, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable] + + [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable] public bool CanEat { get; set; } - [Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(10f, IsPropertySaveable.Yes, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool UsePathFinding { get; set; } - [Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] + [Serialize(1f, IsPropertySaveable.Yes, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] public float PathFinderPriority { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HideInSonar { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HideInThermalGoggles { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float SonarDisruption { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float DistantSonarRange { get; set; } - [Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] + [Serialize(25000f, IsPropertySaveable.Yes, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] public float DisableDistance { get; set; } - [Serialize(10f, true, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] + [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } - public readonly string File; + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -116,7 +117,7 @@ namespace Barotrauma public HealthParams Health { get; private set; } public AIParams AI { get; private set; } - public CharacterParams(string file) + public CharacterParams(CharacterFile file) { File = file; Load(); @@ -124,45 +125,58 @@ namespace Barotrauma protected override string GetName() => "Character Config File"; - public override XElement MainElement => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; + public static XElement CreateVariantXml(XElement selfXml, XElement parentXml) + { + XElement newXml = selfXml.CreateVariantXML(parentXml); + + XElement selfAi = selfXml.GetChildElement("ai"); + XElement parentAi = parentXml.GetChildElement("ai"); + + if (parentAi is null || parentAi.Elements().None() + || selfAi is null || selfAi.Elements().None()) + { + return newXml; + } + + //discard the inherited targets, just keep the new ones + var finalAiElement = newXml.GetChildElement("ai"); + foreach (var finalTarget in finalAiElement!.Elements().ToArray()) + { + finalTarget.Remove(); + } + + foreach (var inheritorTarget in selfAi.Elements()) + { + finalAiElement.Add(new XElement(inheritorTarget)); + } + + return newXml; + } + public bool Load() { - bool success = base.Load(File); - if (doc.Root.IsCharacterVariant()) + UpdatePath(File.Path); + doc = XMLExtensions.TryLoadXml(Path); + Identifier variantOf = MainElement.VariantOf(); + if (!variantOf.IsEmpty) { VariantFile = doc; - var original = CharacterPrefab.FindBySpeciesName(doc.Root.GetAttributeString("inherit", string.Empty)); - success = Load(original.FilePath); - CreateSubParams(); - TryLoadOverride(this, VariantFile.Root, SerializableProperties); - foreach (XElement subElement in VariantFile.Root.Elements()) - { - var matchingParams = SubParams.FirstOrDefault(p => p.Name.Equals(subElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); - if (matchingParams != null) - { - TryLoadOverride(matchingParams, subElement, matchingParams.SerializableProperties); - // TODO: Make recursive? In practice we don't have to go deeper than this, but the implementation would be a lot cleaner with recursion. - foreach (XElement subSubElement in subElement.Elements()) - { - if (subSubElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } - var matchingSubParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); - if (matchingSubParams != null) - { - TryLoadOverride(matchingSubParams, subSubElement, matchingSubParams.SerializableProperties); - } - } - } - } - return success; + #warning TODO: determine that CreateVariantXML is equipped to do this + XElement newRoot = CreateVariantXml(MainElement, CharacterPrefab.FindBySpeciesName(variantOf).ConfigElement); + var oldElement = MainElement; + var parentElement = (XContainer)oldElement.Parent ?? doc; oldElement.Remove(); parentElement.Add(newRoot); } - if (string.IsNullOrEmpty(SpeciesName) && MainElement != null) + IsLoaded = Deserialize(MainElement); + OriginalElement = new XElement(MainElement).FromPackage(Path.ContentPackage); + if (SpeciesName.IsEmpty && MainElement != null) { //backwards compatibility - SpeciesName = MainElement.GetAttributeString("name", ""); + SpeciesName = MainElement.GetAttributeIdentifier("name", ""); } CreateSubParams(); - return success; + return IsLoaded; } public bool Save(string fileNameWithoutExtension = null) @@ -189,7 +203,7 @@ namespace Barotrauma return true; } - public bool CompareGroup(string group) => !string.IsNullOrWhiteSpace(group) && !string.IsNullOrWhiteSpace(Group) && group.Equals(Group, StringComparison.OrdinalIgnoreCase); + public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group; protected void CreateSubParams() { @@ -239,26 +253,14 @@ namespace Barotrauma } } - private void TryLoadOverride(object parentObject, XElement element, Dictionary properties) - { - foreach (var property in properties) - { - var matchingAttribute = element.GetAttribute(property.Key); - if (matchingAttribute != null) - { - property.Value.TrySetValue(parentObject, matchingAttribute.Value); - } - } - } - public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true, bool loadDefaultValues = true) { if (base.Deserialize(element)) { //backwards compatibility - if (string.IsNullOrEmpty(SpeciesName)) + if (SpeciesName.IsEmpty) { - SpeciesName = element.GetAttributeString("name", "[NAME NOT GIVEN]"); + SpeciesName = element.GetAttributeIdentifier("name", "[NAME NOT GIVEN]"); } if (alsoChildren) { @@ -300,9 +302,9 @@ namespace Barotrauma } #endif - public bool AddSound() => TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out _, Sounds); + public bool AddSound() => TryAddSubParam(CreateElement("sound"), (e, c) => new SoundParams(e, c), out _, Sounds); - public void AddInventory() => TryAddSubParam(new XElement("inventory", new XElement("item")), (e, c) => new InventoryParams(e, c), out _, Inventories); + public void AddInventory() => TryAddSubParam(CreateElement("inventory", new XElement("item")), (e, c) => new InventoryParams(e, c), out _, Inventories); public void AddBloodEmitter() => AddEmitter("bloodemitter"); public void AddGibEmitter() => AddEmitter("gibemitter"); @@ -313,13 +315,13 @@ namespace Barotrauma switch (type) { case "gibemitter": - TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters); + TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters); break; case "bloodemitter": - TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters); + TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters); break; case "damageemitter": - TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters); + TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters); break; default: throw new NotImplementedException(type); } @@ -342,7 +344,7 @@ namespace Barotrauma return true; } - protected bool TryAddSubParam(XElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam + protected bool TryAddSubParam(ContentXElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam { subParam = constructor(element, this); if (collection != null && filter != null) @@ -360,24 +362,39 @@ namespace Barotrauma { public override string Name => "Sound"; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string File { get; private set; } #if CLIENT - [Serialize(SoundType.Idle, true), Editable] + [Serialize(SoundType.Idle, IsPropertySaveable.Yes), Editable] public SoundType State { get; private set; } #endif - [Serialize(1000f, true), Editable(minValue: 0f, maxValue: 10000f)] + [Serialize(1000f, IsPropertySaveable.Yes), Editable(minValue: 0f, maxValue: 10000f)] public float Range { get; private set; } - [Serialize(1.0f, true), Editable(minValue: 0f, maxValue: 2.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(minValue: 0f, maxValue: 2.0f)] public float Volume { get; private set; } - [Serialize(Gender.None, true, description: "Is the sound gender specific?"), Editable()] - public Gender Gender { get; private set; } + [Serialize("", IsPropertySaveable.Yes, description: "Which tags are required for this sound to play?"), Editable()] + public string Tags + { + get { return string.Join(',', TagSet); } + private set { TagSet = value.Split(',').ToIdentifiers().ToImmutableHashSet(); } + } - public SoundParams(XElement element, CharacterParams character) : base(element, character) { } + public ImmutableHashSet TagSet { get; private set; } + + public SoundParams(ContentXElement element, CharacterParams character) : base(element, character) + { + HashSet tags = TagSet.ToHashSet(); + Identifier genderFallback = element.GetAttributeIdentifier("gender", ""); + if (genderFallback != Identifier.Empty && genderFallback != "None") + { + tags.Add(genderFallback); + } + TagSet = tags.ToImmutableHashSet(); + } } public class ParticleParams : SubParam @@ -395,83 +412,83 @@ namespace Barotrauma } } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string Particle { get; set; } - [Serialize(0f, true), Editable(-360f, 360f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(-360f, 360f, decimals: 0)] public float AngleMin { get; private set; } - [Serialize(0f, true), Editable(-360f, 360f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(-360f, 360f, decimals: 0)] public float AngleMax { get; private set; } - [Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)] public float ScaleMin { get; private set; } - [Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)] public float ScaleMax { get; private set; } - [Serialize(0f, true), Editable(0f, 10000f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 10000f, decimals: 0)] public float VelocityMin { get; private set; } - [Serialize(0f, true), Editable(0f, 10000f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 10000f, decimals: 0)] public float VelocityMax { get; private set; } - [Serialize(0f, true), Editable(0f, 100f, decimals: 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)] public float EmitInterval { get; private set; } - [Serialize(0, true), Editable(0, 1000)] + [Serialize(0, IsPropertySaveable.Yes), Editable(0, 1000)] public int ParticlesPerSecond { get; private set; } - [Serialize(0, true), Editable(0, 1000)] + [Serialize(0, IsPropertySaveable.Yes), Editable(0, 1000)] public int ParticleAmount { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HighQualityCollisionDetection { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool CopyEntityAngle { get; private set; } - public ParticleParams(XElement element, CharacterParams character) : base(element, character) { } + public ParticleParams(ContentXElement element, CharacterParams character) : base(element, character) { } } public class HealthParams : SubParam { public override string Name => "Health"; - [Serialize(100f, true, description: "How much (max) health does the character have?"), Editable(minValue: 1, maxValue: 10000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How much (max) health does the character have?"), Editable(minValue: 1, maxValue: 10000f)] public float Vitality { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool DoesBleed { get; set; } - [Serialize(float.NegativeInfinity, true), Editable(minValue: float.NegativeInfinity, maxValue: 0)] + [Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)] public float CrushDepth { get; set; } // Make editable? - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool UseHealthWindow { get; set; } - [Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float BleedingReduction { get; private set; } - [Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float BurnReduction { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ConstantHealthRegeneration { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HealthRegenerationWhenEating { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool StunImmunity { get; set; } - [Serialize(false, true, description: "Can afflictions affect the face/body tint of the character."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } // TODO: limbhealths, sprite? - public HealthParams(XElement element, CharacterParams character) : base(element, character) { } + public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { } } public class InventoryParams : SubParam @@ -480,26 +497,26 @@ namespace Barotrauma { public override string Name => "Item"; - [Serialize("", true, description: "Item identifier."), Editable()] + [Serialize("", IsPropertySaveable.Yes, description: "Item identifier."), Editable()] public string Identifier { get; private set; } - public InventoryItem(XElement element, CharacterParams character) : base(element, character) { } + public InventoryItem(ContentXElement element, CharacterParams character) : base(element, character) { } } public override string Name => "Inventory"; - [Serialize("Any, Any", true, description: "Which slots the inventory holds? Accepted types: None, Any, RightHand, LeftHand, Head, InnerClothes, OuterClothes, Headset, and Card."), Editable()] + [Serialize("Any, Any", IsPropertySaveable.Yes, description: "Which slots the inventory holds? Accepted types: None, Any, RightHand, LeftHand, Head, InnerClothes, OuterClothes, Headset, and Card."), Editable()] public string Slots { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AccessibleWhenAlive { get; private set; } - [Serialize(1.0f, true, description: "What are the odds that this inventory is spawned on the character?"), Editable(minValue: 0f, maxValue: 1.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "What are the odds that this inventory is spawned on the character?"), Editable(minValue: 0f, maxValue: 1.0f)] public float Commonness { get; private set; } public List Items { get; private set; } = new List(); - public InventoryParams(XElement element, CharacterParams character) : base(element, character) + public InventoryParams(ContentXElement element, CharacterParams character) : base(element, character) { foreach (var itemElement in element.GetChildElements("item")) { @@ -512,7 +529,7 @@ namespace Barotrauma public void AddItem(string identifier = null) { identifier = identifier ?? ""; - var element = new XElement("item", new XAttribute("identifier", identifier)); + var element = CreateElement("item", new XAttribute("identifier", identifier)); Element.Add(element); var item = new InventoryItem(element, Character); SubParams.Add(item); @@ -526,89 +543,89 @@ namespace Barotrauma { public override string Name => "AI"; - [Serialize(1.0f, true, description: "How strong other characters think this character is? Only affects AI."), Editable()] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How strong other characters think this character is? Only affects AI."), Editable()] public float CombatStrength { get; private set; } - [Serialize(1.0f, true, description: "Affects how far the character can see the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can see the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] public float Sight { get; private set; } - [Serialize(1.0f, true, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] public float Hearing { get; private set; } - [Serialize(100f, true, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)] public float AggressionHurt { get; private set; } - [Serialize(10f, true, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)] + [Serialize(10f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)] public float AggressionGreed { get; private set; } - [Serialize(0f, true, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)] public float FleeHealthThreshold { get; private set; } - [Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()] + [Serialize(false, IsPropertySaveable.Yes, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()] public bool AttackWhenProvoked { get; private set; } - [Serialize(false, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } - [Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] + [Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] public float AvoidTime { get; private set; } - [Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)] + [Serialize(20f, IsPropertySaveable.Yes, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)] public float MinFleeTime { get; private set; } - [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Does the character try to break inside the sub?"), Editable] public bool AggressiveBoarding { get; private set; } - [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable] public bool EnforceAggressiveBehaviorForMissions { get; private set; } - [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the character target or ignore walls when it's outside the submarine."), Editable] public bool TargetOuterWalls { get; private set; } - [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] public bool RandomAttack { get; private set; } - [Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } - [Serialize(false, true, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] public bool KeepDoorsClosed { get; private set; } - [Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] + [Serialize(true, IsPropertySaveable.Yes, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] public bool AvoidAbyss { get; set; } - [Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] + [Serialize(false, IsPropertySaveable.Yes, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] public bool StayInAbyss { get; set; } - - [Serialize(false, true, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable] + + [Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable] public bool PatrolFlooded { get; set; } - [Serialize(false, true, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] public bool PatrolDry { get; set; } - [Serialize(0f, true, description: ""), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] public float StartAggression { get; private set; } - [Serialize(100f, true, description: ""), Editable] + [Serialize(100f, IsPropertySaveable.Yes, description: ""), Editable] public float MaxAggression { get; private set; } - [Serialize(0f, true, description: ""), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] public float AggressionCumulation { get; private set; } - [Serialize(WallTargetingMethod.Target, true, description: ""), Editable] + [Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: ""), Editable] public WallTargetingMethod WallTargetingMethod { get; private set; } public IEnumerable Targets => targets; protected readonly List targets = new List(); - public AIParams(XElement element, CharacterParams character) : base(element, character) + public AIParams(ContentXElement element, CharacterParams character) : base(element, character) { if (element == null) { return; } element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _)); element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _)); } - private bool TryAddTarget(XElement targetElement, out TargetParams target) + private bool TryAddTarget(ContentXElement targetElement, out TargetParams target) { string tag = targetElement.GetAttributeString("tag", null); if (HasTag(tag)) @@ -628,9 +645,12 @@ namespace Barotrauma public bool TryAddEmptyTarget(out TargetParams targetParams) => TryAddNewTarget("newtarget" + targets.Count, AIState.Attack, 0f, out targetParams); - public bool TryAddNewTarget(string tag, AIState state, float priority, out TargetParams targetParams) + public bool TryAddNewTarget(string tag, AIState state, float priority, out TargetParams targetParams) => + TryAddNewTarget(tag.ToIdentifier(), state, priority, out targetParams); + + public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams) { - var element = TargetParams.CreateNewElement(tag, state, priority); + var element = TargetParams.CreateNewElement(Character, tag, state, priority); if (TryAddTarget(element, out targetParams)) { Element.Add(element); @@ -642,17 +662,22 @@ namespace Barotrauma } } - public bool HasTag(string tag) + public bool HasTag(string tag) => HasTag(tag.ToIdentifier()); + + public bool HasTag(Identifier tag) { if (tag == null) { return false; } - return targets.Any(t => t.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase)); + return targets.Any(t => t.Tag == tag); } public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets); public bool TryGetTarget(string targetTag, out TargetParams target) + => TryGetTarget(targetTag.ToIdentifier(), out target); + + public bool TryGetTarget(Identifier targetTag, out TargetParams target) { - target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetTag, StringComparison.OrdinalIgnoreCase)); + target = targets.FirstOrDefault(t => t.Tag == targetTag); return target != null; } @@ -665,7 +690,7 @@ namespace Barotrauma return target != null; } - public bool TryGetTarget(IEnumerable tags, out TargetParams target) + public bool TryGetTarget(IEnumerable tags, out TargetParams target) { target = null; if (tags == null || tags.None()) { return false; } @@ -674,7 +699,7 @@ namespace Barotrauma { if (potentialTarget.Priority > priority) { - if (tags.Any(t => string.Equals(t, potentialTarget.Tag, StringComparison.OrdinalIgnoreCase))) + if (tags.Any(t => t == potentialTarget.Tag)) { target = potentialTarget; priority = target.Priority; @@ -685,7 +710,11 @@ namespace Barotrauma } public TargetParams GetTarget(string targetTag, bool throwError = true) + => GetTarget(targetTag.ToIdentifier(), throwError); + + public TargetParams GetTarget(Identifier targetTag, bool throwError = true) { + if (targetTag.IsEmpty) { return null; } if (!TryGetTarget(targetTag, out TargetParams target)) { if (throwError) @@ -701,102 +730,108 @@ namespace Barotrauma { public override string Name => "Target"; - [Serialize("", true, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()] + [Serialize("", IsPropertySaveable.Yes, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()] public string Tag { get; private set; } - [Serialize(AIState.Idle, true), Editable] + [Serialize(AIState.Idle, IsPropertySaveable.Yes), Editable] public AIState State { get; set; } - [Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)] public float Priority { get; set; } - [Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] public float ReactDistance { get; set; } - [Serialize(0f, true, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] public float AttackDistance { get; set; } - [Serialize(0f, true, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable] public float Timer { get; set; } - [Serialize(false, true, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable] public bool IgnoreContained { get; set; } - [Serialize(false, true, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable] public bool IgnoreInside { get; set; } - [Serialize(false, true, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] public bool IgnoreOutside { get; set; } - [Serialize(false, true, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] public bool IgnoreIfNotInSameSub { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool IgnoreIncapacitated { get; set; } - [Serialize(0f, true, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable] public float Threshold { get; private set; } - [Serialize(-1f, true, description: "A generic min threshold. Not used if set to negative."), Editable] + [Serialize(-1f, IsPropertySaveable.Yes, description: "A generic min threshold. Not used if set to negative."), Editable] public float ThresholdMin { get; private set; } - [Serialize(-1f, true, description: "A generic max threshold. Not used if set to negative."), Editable] + [Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable] public float ThresholdMax { get; private set; } - [Serialize("0.0, 0.0", true), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable] public Vector2 Offset { get; private set; } - [Serialize(AttackPattern.Straight, true), Editable] + [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable] public AttackPattern AttackPattern { get; set; } #region Sweep - [Serialize(0f, true, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] public float SweepDistance { get; private set; } - [Serialize(10f, true, description: "How much the sweep affects the steering?"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1f, DecimalCount = 1)] + [Serialize(10f, IsPropertySaveable.Yes, description: "How much the sweep affects the steering?"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1f, DecimalCount = 1)] public float SweepStrength { get; private set; } - [Serialize(1f, true, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] public float SweepSpeed { get; private set; } #endregion #region Circle - [Serialize(5000f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] + [Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] public float CircleStartDistance { get; private set; } - [Serialize(1f, true), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] public float CircleRotationSpeed { get; private set; } - [Serialize(5f, true), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] + [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] public float CircleStrikeDistanceMultiplier { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] public float CircleMaxRandomOffset { get; private set; } #endregion - public TargetParams(XElement element, CharacterParams character) : base(element, character) { } + public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) { } - public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { } + public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(character, tag, state, priority), character) { } - public static XElement CreateNewElement(string tag, AIState state, float priority) + public static ContentXElement CreateNewElement(CharacterParams character, Identifier tag, AIState state, float priority) => + CreateNewElement(character, tag.Value, state, priority); + + public static ContentXElement CreateNewElement(CharacterParams character, string tag, AIState state, float priority) { return new XElement("target", new XAttribute("tag", tag), new XAttribute("state", state), - new XAttribute("priority", priority)); + new XAttribute("priority", priority)).FromPackage(character.File.ContentPackage); } } public abstract class SubParam : ISerializableEntity { public virtual string Name { get; set; } - public Dictionary SerializableProperties { get; private set; } - public XElement Element { get; set; } + public Dictionary SerializableProperties { get; private set; } + public ContentXElement Element { get; set; } public List SubParams { get; set; } = new List(); public CharacterParams Character { get; private set; } - public SubParam(XElement element, CharacterParams character) + protected ContentXElement CreateElement(string name, params object[] attrs) + => new XElement(name, attrs).FromPackage(Element.ContentPackage); + + public SubParam(ContentXElement element, CharacterParams character) { Element = element; Character = character; @@ -843,12 +878,12 @@ namespace Barotrauma #if CLIENT public SerializableEntityEditor SerializableEntityEditor { get; protected set; } - public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, ScalableFont titleFont = null) + public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, GUIFont titleFont = null) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUIStyle.LargeFont); if (recursive) { - SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUI.SmallFont)); + SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUIStyle.SmallFont)); } if (space > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 77fff0fd0..76e138579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Xml.Linq; using Microsoft.Xna.Framework; +using File = Barotrauma.IO.File; #if DEBUG using System.IO; using System.Xml; @@ -16,11 +17,13 @@ namespace Barotrauma public string Name { get; private set; } public string FileName { get; private set; } public string Folder { get; private set; } - public string FullPath { get; private set; } - public Dictionary SerializableProperties { get; protected set; } + public ContentPath Path { get; protected set; } = ContentPath.Empty; + public Dictionary SerializableProperties { get; protected set; } + protected ContentXElement rootElement; protected XDocument doc; - public XDocument Doc + + private XDocument Doc { get { @@ -31,16 +34,30 @@ namespace Barotrauma } return doc; } - protected set + set { doc = value; } } - public virtual XElement MainElement => doc.Root; - public XElement OriginalElement { get; protected set; } + public virtual ContentXElement MainElement + { + get + { + if (rootElement?.Element != doc.Root) + { + rootElement = doc.Root.FromPackage(Path.ContentPackage); + } + return rootElement; + } + } + + public ContentXElement OriginalElement { get; protected set; } - protected virtual string GetName() => Path.GetFileNameWithoutExtension(FullPath).FormatCamelCaseWithSpaces(); + protected ContentXElement CreateElement(string name, params object[] attrs) + => new XElement(name, attrs).FromPackage(Path.ContentPackage); + + protected virtual string GetName() => System.IO.Path.GetFileNameWithoutExtension(Path.Value).FormatCamelCaseWithSpaces(); protected virtual bool Deserialize(XElement element = null) { @@ -61,22 +78,22 @@ namespace Barotrauma return true; } - protected virtual bool Load(string file) + protected virtual bool Load(ContentPath file) { UpdatePath(file); - doc = XMLExtensions.TryLoadXml(FullPath); + doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { return false; } IsLoaded = Deserialize(MainElement); - OriginalElement = new XElement(MainElement); + OriginalElement = new XElement(MainElement).FromPackage(MainElement.ContentPackage); return IsLoaded; } - protected virtual void UpdatePath(string fullPath) + protected virtual void UpdatePath(ContentPath fullPath) { - FullPath = fullPath; + Path = fullPath; Name = GetName(); - FileName = Path.GetFileName(FullPath); - Folder = Path.GetDirectoryName(FullPath); + FileName = System.IO.Path.GetFileName(Path.Value); + Folder = System.IO.Path.GetDirectoryName(Path.Value); } public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null) @@ -98,9 +115,9 @@ namespace Barotrauma } if (fileNameWithoutExtension != null) { - UpdatePath(Path.Combine(Folder, $"{fileNameWithoutExtension}.xml")); + UpdatePath(ContentPath.FromRaw(Path.ContentPackage, System.IO.Path.Combine(Folder, $"{fileNameWithoutExtension}.xml"))); } - using (var writer = XmlWriter.Create(FullPath, settings)) + using (var writer = XmlWriter.Create(Path.Value, settings)) { Doc.WriteTo(writer); writer.Flush(); @@ -112,7 +129,7 @@ namespace Barotrauma { if (forceReload) { - return Load(FullPath); + return Load(Path); } return Deserialize(OriginalElement); } @@ -126,7 +143,7 @@ namespace Barotrauma DebugConsole.ThrowError("[Params] Not loaded!"); return; } - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUIStyle.LargeFont); if (space > 0) { new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 3d2c05297..c25fa3367 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -14,13 +14,13 @@ namespace Barotrauma { class HumanRagdollParams : RagdollParams { - public static HumanRagdollParams GetRagdollParams(string speciesName, string fileName = null) => GetRagdollParams(speciesName, fileName); - public static HumanRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); + public static HumanRagdollParams GetRagdollParams(Identifier speciesName, string fileName = null) => GetRagdollParams(speciesName, fileName); + public static HumanRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); } class FishRagdollParams : RagdollParams { - public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); + public static FishRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); } class RagdollParams : EditableParams, IMemorizable @@ -29,15 +29,15 @@ namespace Barotrauma public const float MIN_SCALE = 0.1f; public const float MAX_SCALE = 2; - public string SpeciesName { get; private set; } + public Identifier SpeciesName { get; private set; } - [Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable] public string Texture { get; set; } - - [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } - - [Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] public float SpritesheetOrientation { get; set; } public bool IsSpritesheetOrientationHorizontal @@ -51,85 +51,85 @@ namespace Barotrauma } private float limbScale; - [Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } private float jointScale; - [Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } // Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize? - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float TextureScale { get; set; } - [Serialize(45f, true, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)] + [Serialize(45f, IsPropertySaveable.Yes, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)] public float ColliderHeightFromFloor { get; set; } - [Serialize(50f, true, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(50f, IsPropertySaveable.Yes, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float ImpactTolerance { get; set; } - [Serialize(true, true, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] + [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] public bool CanEnterSubmarine { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool CanWalk { get; set; } - [Serialize(true, true, description: "Can the character be dragged around by other creatures?"), Editable()] + [Serialize(true, IsPropertySaveable.Yes, description: "Can the character be dragged around by other creatures?"), Editable()] public bool Draggable { get; set; } - [Serialize(LimbType.Torso, true), Editable] + [Serialize(LimbType.Torso, IsPropertySaveable.Yes), Editable] public LimbType MainLimb { get; set; } - private readonly static Dictionary> allRagdolls = new Dictionary>(); + /// + /// key1: Species name + /// key2: File path + /// value: Ragdoll parameters + /// + private readonly static Dictionary> allRagdolls = new Dictionary>(); public List Colliders { get; private set; } = new List(); public List Limbs { get; private set; } = new List(); public List Joints { get; private set; } = new List(); protected IEnumerable GetAllSubParams() => - Colliders.Select(c => c as SubParam) - .Concat(Limbs.Select(j => j as SubParam) - .Concat(Joints.Select(j => j as SubParam))); + Colliders + .Concat(Limbs) + .Concat(Joints); - public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll"; - public static string GetDefaultFile(string speciesName, ContentPackage contentPackage = null) - => Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml"); + public static string GetDefaultFileName(Identifier speciesName) => $"{speciesName.Value.CapitaliseFirstInvariant()}DefaultRagdoll"; + public static string GetDefaultFile(Identifier speciesName, ContentPackage contentPackage = null) + => IO.Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml"); - public static string GetFolder(string speciesName, ContentPackage contentPackage = null) + public static string GetFolder(Identifier speciesName, ContentPackage contentPackage = null) { - CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage)); - if (prefab?.XDocument == null) + CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage)); + if (prefab?.ConfigElement == null) { DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})"); return string.Empty; } - return GetFolder(prefab.XDocument, prefab.FilePath); + return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value); } - public static string GetFolder(XDocument doc, string filePath) + private static string GetFolder(ContentXElement root, string filePath) { - var root = doc.Root; - if (root?.IsOverride() ?? false) + var folder = root?.GetChildElement("ragdolls")?.GetAttributeContentPath("folder")?.Value; + if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { - root = root.FirstElement(); - } - var folder = root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty); - if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) - { - folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar; + folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Ragdolls") + IO.Path.DirectorySeparatorChar; } return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } - public static T GetDefaultRagdollParams(string speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); + public static T GetDefaultRagdollParams(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); /// /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! /// If a custom folder is used, it's defined in the character info file. /// - public static T GetRagdollParams(string speciesName, string fileName = null) where T : RagdollParams, new() + public static T GetRagdollParams(Identifier speciesName, string fileName = null) where T : RagdollParams, new() { - if (string.IsNullOrWhiteSpace(speciesName)) + if (speciesName.IsEmpty) { throw new Exception($"Species name null or empty!"); } @@ -138,66 +138,88 @@ namespace Barotrauma ragdolls = new Dictionary(); allRagdolls.Add(speciesName, ragdolls); } - if (string.IsNullOrEmpty(fileName) || !ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) + + if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) { - string selectedFile = null; - string folder = GetFolder(speciesName); - if (Directory.Exists(folder)) + return (T)ragdoll; + } + + string selectedFile = null; + + void tryFolderForSpecies(Identifier species, out string err) + { + err = null; + string folder = GetFolder(species); + if (!Directory.Exists(folder)) { - List files = Directory.GetFiles(folder).ToList(); - if (files.None()) - { - DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."); - selectedFile = GetDefaultFile(speciesName); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified - selectedFile = GetDefaultFile(speciesName); - } - else - { - selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); - if (selectedFile == null) - { - DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."); - selectedFile = GetDefaultFile(speciesName); - } - } + err = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; + selectedFile = GetDefaultFile(species); + return; + } + + string[] files = Directory.GetFiles(folder); + if (files.None()) + { + err = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; + selectedFile = GetDefaultFile(species); + } + else if (string.IsNullOrEmpty(fileName)) + { + // Files found, but none specified + selectedFile = GetDefaultFile(species); } else { - DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."); - selectedFile = GetDefaultFile(speciesName); - } - if (selectedFile == null) - { - throw new Exception("[RagdollParams] Selected file null!"); - } - DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); - T r = new T(); - if (r.Load(selectedFile, speciesName)) - { - if (!ragdolls.ContainsKey(r.Name)) + selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + if (selectedFile == null) { - ragdolls.Add(r.Name, r); + err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; + selectedFile = GetDefaultFile(species); } - return r; - } - else - { - // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early. - throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."); } } - return (T)ragdoll; + + tryFolderForSpecies(speciesName, out var error); + Identifier parentSpeciesName = CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab) + ? prefab.VariantOf + : Identifier.Empty; + if (!error.IsNullOrEmpty() && !parentSpeciesName.IsEmpty) + { + tryFolderForSpecies(parentSpeciesName, out error); + } + + if (!error.IsNullOrEmpty()) + { + DebugConsole.ThrowError(error); + } + + if (selectedFile == null) + { + throw new Exception("[RagdollParams] Selected file null!"); + } + DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); + var characterPrefab = CharacterPrefab.Prefabs[speciesName]; + T r = new T(); + if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) + { + if (!ragdolls.ContainsKey(r.Name)) + { + ragdolls.Add(r.Name, r); + } + return r; + } + else + { + // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early. + throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."); + } } /// /// Creates a default ragdoll for the species using a predefined configuration. /// Note: Use only to create ragdolls for new characters, because this overrides the old ragdoll! /// - public static T CreateDefault(string fullPath, string speciesName, XElement mainElement) where T : RagdollParams, new() + public static T CreateDefault(string fullPath, Identifier speciesName, XElement mainElement) where T : RagdollParams, new() { // Remove the old ragdolls, if found. if (allRagdolls.ContainsKey(speciesName)) @@ -211,10 +233,12 @@ namespace Barotrauma { doc = new XDocument(mainElement) }; - instance.UpdatePath(fullPath); + var characterPrefab = CharacterPrefab.Prefabs[speciesName]; + var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath); + instance.UpdatePath(contentPath); instance.IsLoaded = instance.Deserialize(mainElement); instance.Save(); - instance.Load(fullPath, speciesName); + instance.Load(contentPath, speciesName); ragdolls.Add(instance.Name, instance); DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite); return instance as T; @@ -222,7 +246,7 @@ namespace Barotrauma public static void ClearCache() => allRagdolls.Clear(); - protected override void UpdatePath(string fullPath) + protected override void UpdatePath(ContentPath fullPath) { if (SpeciesName == null) { @@ -259,7 +283,7 @@ namespace Barotrauma }); } - protected bool Load(string file, string speciesName) + protected bool Load(ContentPath file, Identifier speciesName) { if (Load(file)) { @@ -287,7 +311,7 @@ namespace Barotrauma { if (forceReload) { - return Load(FullPath, SpeciesName); + return Load(Path, SpeciesName); } // Don't use recursion, because the reset method might be overriden Deserialize(OriginalElement, alsoChildren: false, recursive: false); @@ -401,8 +425,10 @@ namespace Barotrauma } var copy = new RagdollParams { + SpeciesName = SpeciesName, IsLoaded = true, - doc = new XDocument(doc) + doc = new XDocument(doc), + Path = Path }; copy.CreateColliders(); copy.CreateLimbs(); @@ -453,7 +479,7 @@ namespace Barotrauma public class JointParams : SubParam { private string name; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public override string Name { get @@ -472,61 +498,61 @@ namespace Barotrauma public override string GenerateName() => $"Joint {Limb1} - {Limb2}"; - [Serialize(-1, true), Editable] + [Serialize(-1, IsPropertySaveable.Yes), Editable] public int Limb1 { get; set; } - [Serialize(-1, true), Editable] + [Serialize(-1, IsPropertySaveable.Yes), Editable] public int Limb2 { get; set; } /// /// Should be converted to sim units. /// - [Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb1."), Editable()] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "Local position of the joint in the Limb1."), Editable()] public Vector2 Limb1Anchor { get; set; } /// /// Should be converted to sim units. /// - [Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb2."), Editable()] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "Local position of the joint in the Limb2."), Editable()] public Vector2 Limb2Anchor { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool CanBeSevered { get; set; } - [Serialize(0f, true, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] public float SeveranceProbabilityModifier { get; set; } - [Serialize("gore", true), Editable] + [Serialize("gore", IsPropertySaveable.Yes), Editable] public string BreakSound { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool LimitEnabled { get; set; } /// /// In degrees. /// - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float UpperLimit { get; set; } /// /// In degrees. /// - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float LowerLimit { get; set; } - [Serialize(0.25f, true), Editable] + [Serialize(0.25f, IsPropertySaveable.Yes), Editable] public float Stiffness { get; set; } - [Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable] public float Scale { get; set; } - [Serialize(false, false), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.No), Editable(ReadOnly = true)] public bool WeldJoint { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool ClockWiseRotation { get; set; } - public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public JointParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } public class LimbParams : SubParam @@ -542,7 +568,7 @@ namespace Barotrauma public List DamageModifiers { get; private set; } = new List(); private string name; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public override string Name { get @@ -563,10 +589,10 @@ namespace Barotrauma public SpriteParams GetSprite() => deformSpriteParams ?? normalSpriteParams; - [Serialize(-1, true), Editable(ReadOnly = true)] + [Serialize(-1, IsPropertySaveable.Yes), Editable(ReadOnly = true)] public int ID { get; set; } - [Serialize(LimbType.None, true, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()] public LimbType Type { get; set; } /// @@ -576,136 +602,136 @@ namespace Barotrauma public float GetSpriteOrientationInDegrees() => float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string Notes { get; set; } - [Serialize(1f, true), Editable(DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float Scale { get; set; } - [Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()] + [Serialize(true, IsPropertySaveable.Yes, description: "Does the limb flip when the character flips?"), Editable()] public bool Flip { get; set; } - [Serialize(false, true, description: "Currently only works with non-deformable (normal) sprites."), Editable()] + [Serialize(false, IsPropertySaveable.Yes, description: "Currently only works with non-deformable (normal) sprites."), Editable()] public bool MirrorVertically { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool MirrorHorizontally { get; set; } - [Serialize(false, true, description: "Disable drawing for this limb."), Editable()] + [Serialize(false, IsPropertySaveable.Yes, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] + [Serialize(float.NaN, IsPropertySaveable.Yes, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] public float SpriteOrientation { get; set; } - [Serialize(LimbType.None, true, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")] public LimbType InheritLimbDepth { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float SteerForce { get; set; } - [Serialize(0f, true, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Radius { get; set; } - [Serialize(0f, true, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Height { get; set; } - [Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] public float Density { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool IgnoreCollisions { get; set; } - [Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable] + [Serialize(7f, IsPropertySaveable.Yes, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable] public float AngularDamping { get; set; } - [Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)] + [Serialize(1f, IsPropertySaveable.Yes, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)] public float AttackPriority { get; set; } - [Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()] + [Serialize("0, 0", IsPropertySaveable.Yes, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()] public Vector2 PullPos { get; set; } - [Serialize("0, 0", true, description: "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint."), Editable()] + [Serialize("0, 0", IsPropertySaveable.Yes, description: "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint."), Editable()] public Vector2 StepOffset { get; set; } - [Serialize(-1, true, description: "The id of the refecence joint. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter. Only applicable if this limb is a foot."), Editable()] + [Serialize(-1, IsPropertySaveable.Yes, description: "The id of the refecence joint. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter. Only applicable if this limb is a foot."), Editable()] public int RefJoint { get; set; } - [Serialize("0, 0", true, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)] + [Serialize("0, 0", IsPropertySaveable.Yes, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)] public Vector2 MouthPos { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float ConstantTorque { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float ConstantAngle { get; set; } - [Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)] public float AttackForceMultiplier { get; set; } - [Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(1f, IsPropertySaveable.Yes, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] public float MinSeveranceDamage { get; set; } - [Serialize(true, true, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable] public bool CanBeSeveredAlive { get; set; } //how long it takes for severed limbs to fade out - [Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)] + [Serialize(10f, IsPropertySaveable.Yes, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)] public float SeveredFadeOutTime { get; set; } = 10.0f; - [Serialize(false, true, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable] public bool ApplyTailAngle { get; set; } - [Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)] public float SineFrequencyMultiplier { get; set; } - [Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)] public float SineAmplitudeMultiplier { get; set; } - [Serialize(0f, true), Editable(0, 100, ValueStep = 1, DecimalCount = 1)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0, 100, ValueStep = 1, DecimalCount = 1)] public float BlinkFrequency { get; set; } - [Serialize(0.2f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] + [Serialize(0.2f, IsPropertySaveable.Yes), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] public float BlinkDurationIn { get; set; } - [Serialize(0.5f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] public float BlinkDurationOut { get; set; } - [Serialize(0f, true), Editable(0, 10, ValueStep = 1, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0, 10, ValueStep = 1, DecimalCount = 2)] public float BlinkHoldTime { get; set; } - [Serialize(0f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] public float BlinkRotationIn { get; set; } - [Serialize(45f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] + [Serialize(45f, IsPropertySaveable.Yes), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] public float BlinkRotationOut { get; set; } - [Serialize(50f, true), Editable] + [Serialize(50f, IsPropertySaveable.Yes), Editable] public float BlinkForce { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool OnlyBlinkInWater { get; set; } - [Serialize(TransitionMode.Linear, true), Editable] + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable] public TransitionMode BlinkTransitionIn { get; private set; } - [Serialize(TransitionMode.Linear, true), Editable] + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable] public TransitionMode BlinkTransitionOut { get; private set; } // Non-editable -> // TODO: make read-only - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int HealthIndex { get; set; } - [Serialize(0.3f, true)] + [Serialize(0.3f, IsPropertySaveable.Yes)] public float Friction { get; set; } - [Serialize(0.05f, true)] + [Serialize(0.05f, IsPropertySaveable.Yes)] public float Restitution { get; set; } - public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { var spriteElement = element.GetChildElement("sprite"); if (spriteElement != null) @@ -761,7 +787,7 @@ namespace Barotrauma public bool AddAttack() { if (Attack != null) { return false; } - TryAddSubParam(new XElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack); + TryAddSubParam(CreateElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack); Attack = newAttack; return Attack != null; } @@ -770,7 +796,7 @@ namespace Barotrauma public bool AddSound() { if (Sound != null) { return false; } - TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound); + TryAddSubParam(CreateElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound); Sound = newSound; return Sound != null; } @@ -778,14 +804,14 @@ namespace Barotrauma public bool AddLight() { if (LightSource != null) { return false; } - var lightSourceElement = new XElement("lightsource", + var lightSourceElement = CreateElement("lightsource", new XElement("lighttexture", new XAttribute("texture", "Content/Lights/pointlight_bright.png"))); TryAddSubParam(lightSourceElement, (e, c) => new LightSourceParams(e, c), out LightSourceParams newLightSource); LightSource = newLightSource; return LightSource != null; } - public bool AddDamageModifier() => TryAddSubParam(new XElement("damagemodifier"), (e, c) => new DamageModifierParams(e, c), out _, DamageModifiers); + public bool AddDamageModifier() => TryAddSubParam(CreateElement("damagemodifier"), (e, c) => new DamageModifierParams(e, c), out _, DamageModifiers); public bool RemoveAttack() { @@ -819,7 +845,7 @@ namespace Barotrauma public bool RemoveDamageModifier(DamageModifierParams damageModifier) => RemoveSubParam(damageModifier, DamageModifiers); - protected bool TryAddSubParam(XElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam + protected bool TryAddSubParam(ContentXElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam { subParam = constructor(element, Ragdoll); if (collection != null && filter != null) @@ -846,7 +872,7 @@ namespace Barotrauma public class DecorativeSpriteParams : SpriteParams { - public DecorativeSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DecorativeSpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { #if CLIENT DecorativeSprite = new DecorativeSprite(element); @@ -882,7 +908,7 @@ namespace Barotrauma { public DeformationParams Deformation { get; private set; } - public DeformSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DeformSpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { Deformation = new DeformationParams(element, ragdoll); SubParams.Add(Deformation); @@ -891,40 +917,40 @@ namespace Barotrauma public class SpriteParams : SubParam { - [Serialize("0, 0, 0, 0", true), Editable] + [Serialize("0, 0, 0, 0", IsPropertySaveable.Yes), Editable] public Rectangle SourceRect { get; set; } - [Serialize("0.5, 0.5", true, description: "The origin of the sprite relative to the collider."), Editable(DecimalCount = 3)] + [Serialize("0.5, 0.5", IsPropertySaveable.Yes, description: "The origin of the sprite relative to the collider."), Editable(DecimalCount = 3)] public Vector2 Origin { get; set; } - [Serialize(0f, true, description: "The Z-depth of the limb relative to other limbs of the same character. 1 is front, 0 is behind."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 3)] + [Serialize(0f, IsPropertySaveable.Yes, description: "The Z-depth of the limb relative to other limbs of the same character. 1 is front, 0 is behind."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 3)] public float Depth { get; set; } - [Serialize("", true), Editable()] + [Serialize("", IsPropertySaveable.Yes), Editable()] public string Texture { get; set; } - [Serialize(false, true), Editable()] + [Serialize(false, IsPropertySaveable.Yes), Editable()] public bool IgnoreTint { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, description: "Target color when the character is dead."), Editable()] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Target color when the character is dead."), Editable()] public Color DeadColor { get; set; } - [Serialize(0f, true, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(0f, IsPropertySaveable.Yes, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)] public float DeadColorTime { get; set; } public override string Name => "Sprite"; - public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public SpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } public string GetTexturePath() => string.IsNullOrWhiteSpace(Texture) ? Ragdoll.Texture : Texture; } public class DeformationParams : SubParam { - public DeformationParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DeformationParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { #if CLIENT Deformations = new Dictionary(); @@ -991,7 +1017,7 @@ namespace Barotrauma public class ColliderParams : SubParam { private string name; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public override string Name { get @@ -1008,16 +1034,16 @@ namespace Barotrauma } } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Radius { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Height { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) + public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) { Name = name; } @@ -1029,16 +1055,16 @@ namespace Barotrauma { public override string Name => "Light Texture"; - [Serialize("Content/Lights/pointlight_bright.png", true), Editable] + [Serialize("Content/Lights/pointlight_bright.png", IsPropertySaveable.Yes), Editable] public string Texture { get; private set; } - [Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)] + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 Origin { get; set; } - [Serialize("1.0, 1.0", true), Editable(DecimalCount = 2)] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 Size { get; set; } - public LightTexture(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public LightTexture(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } public LightTexture Texture { get; private set; } @@ -1047,7 +1073,7 @@ namespace Barotrauma public Lights.LightSourceParams LightSource { get; private set; } #endif - public LightSourceParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public LightSourceParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { #if CLIENT LightSource = new Lights.LightSourceParams(element); @@ -1088,9 +1114,10 @@ namespace Barotrauma { public Attack Attack { get; private set; } - public AttackParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public AttackParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { - Attack = new Attack(element, ragdoll.SpeciesName); + var prefab = CharacterPrefab.Prefabs[ragdoll.SpeciesName]; + Attack = new Attack(element, ragdoll.SpeciesName.Value); } public override bool Deserialize(XElement element = null, bool recursive = true) @@ -1117,7 +1144,7 @@ namespace Barotrauma public bool AddNewAffliction() { Serialize(); - var subElement = new XElement("affliction", + var subElement = CreateElement("affliction", new XAttribute("identifier", "internaldamage"), new XAttribute("strength", 0f), new XAttribute("probability", 1.0f)); @@ -1140,9 +1167,9 @@ namespace Barotrauma { public DamageModifier DamageModifier { get; private set; } - public DamageModifierParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DamageModifierParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { - DamageModifier = new DamageModifier(element, ragdoll.SpeciesName); + DamageModifier = new DamageModifier(element, ragdoll.SpeciesName.Value); } public override bool Deserialize(XElement element = null, bool recursive = true) @@ -1170,24 +1197,27 @@ namespace Barotrauma { public override string Name => "Sound"; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string Tag { get; private set; } - public SoundParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public SoundParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } public abstract class SubParam : ISerializableEntity { public virtual string Name { get; set; } - public Dictionary SerializableProperties { get; private set; } - public XElement Element { get; set; } + public Dictionary SerializableProperties { get; private set; } + public ContentXElement Element { get; set; } public XElement OriginalElement { get; protected set; } public List SubParams { get; set; } = new List(); public RagdollParams Ragdoll { get; private set; } public virtual string GenerateName() => Element.Name.ToString(); - public SubParam(XElement element, RagdollParams ragdoll) + protected ContentXElement CreateElement(string name, params object[] attrs) + => new XElement(name, attrs).FromPackage(Element.ContentPackage); + + public SubParam(ContentXElement element, RagdollParams ragdoll) { Element = element; OriginalElement = new XElement(element); @@ -1226,7 +1256,7 @@ namespace Barotrauma public virtual void Reset() { // Don't use recursion, because the reset method might be overriden - Deserialize(OriginalElement, false); + Deserialize(OriginalElement, recursive: false); SubParams.ForEach(sp => sp.Reset()); } @@ -1235,21 +1265,21 @@ namespace Barotrauma public Dictionary AfflictionEditors { get; private set; } public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); if (this is DecorativeSpriteParams decSpriteParams) { - new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUI.LargeFont); + new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } else if (this is DeformSpriteParams deformSpriteParams) { foreach (var deformation in deformSpriteParams.Deformation.Deformations.Keys) { - new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, deformation, inGame: false, showName: true, titleFont: GUI.LargeFont); + new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, deformation, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } } else if (this is AttackParams attackParams) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); if (AfflictionEditors == null) { AfflictionEditors = new Dictionary(); @@ -1267,11 +1297,11 @@ namespace Barotrauma } else if (this is LightSourceParams lightParams) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } else if (this is DamageModifierParams damageModifierParams) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } if (recursive) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index f023b51f7..579ef14a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -5,20 +5,17 @@ using System.Xml.Linq; namespace Barotrauma { - class SkillSettings : ISerializableEntity + class SkillSettings : Prefab, ISerializableEntity { - public static SkillSettings Current - { - get; - private set; - } + public readonly static PrefabSelector Prefabs = new PrefabSelector(); + public static SkillSettings Current => Prefabs.ActivePrefab; - [Serialize(4.0f, true)] + [Serialize(4.0f, IsPropertySaveable.Yes)] public float SingleRoundSkillGainMultiplier { get; set; } private float skillIncreasePerRepair; - [Serialize(5.0f, true)] + [Serialize(5.0f, IsPropertySaveable.Yes)] public float SkillIncreasePerRepair { get { return skillIncreasePerRepair * GetCurrentSkillGainMultiplier(); } @@ -26,7 +23,7 @@ namespace Barotrauma } private float skillIncreasePerSabotage; - [Serialize(3.0f, true)] + [Serialize(3.0f, IsPropertySaveable.Yes)] public float SkillIncreasePerSabotage { get { return skillIncreasePerSabotage * GetCurrentSkillGainMultiplier(); } @@ -34,7 +31,7 @@ namespace Barotrauma } private float skillIncreasePerCprRevive; - [Serialize(0.5f, true)] + [Serialize(0.5f, IsPropertySaveable.Yes)] public float SkillIncreasePerCprRevive { get { return skillIncreasePerCprRevive * GetCurrentSkillGainMultiplier(); } @@ -42,7 +39,7 @@ namespace Barotrauma } private float skillIncreasePerRepairedStructureDamage; - [Serialize(0.0025f, true)] + [Serialize(0.0025f, IsPropertySaveable.Yes)] public float SkillIncreasePerRepairedStructureDamage { get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); } @@ -50,7 +47,7 @@ namespace Barotrauma } private float skillIncreasePerSecondWhenSteering; - [Serialize(0.005f, true)] + [Serialize(0.005f, IsPropertySaveable.Yes)] public float SkillIncreasePerSecondWhenSteering { get { return skillIncreasePerSecondWhenSteering * GetCurrentSkillGainMultiplier(); } @@ -58,7 +55,7 @@ namespace Barotrauma } private float skillIncreasePerFabricatorRequiredSkill; - [Serialize(0.5f, true)] + [Serialize(0.5f, IsPropertySaveable.Yes)] public float SkillIncreasePerFabricatorRequiredSkill { get { return skillIncreasePerFabricatorRequiredSkill * GetCurrentSkillGainMultiplier(); } @@ -66,7 +63,7 @@ namespace Barotrauma } private float skillIncreasePerHostileDamage; - [Serialize(0.01f, true)] + [Serialize(0.01f, IsPropertySaveable.Yes)] public float SkillIncreasePerHostileDamage { get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); } @@ -74,7 +71,7 @@ namespace Barotrauma } private float skillIncreasePerSecondWhenOperatingTurret; - [Serialize(0.001f, true)] + [Serialize(0.001f, IsPropertySaveable.Yes)] public float SkillIncreasePerSecondWhenOperatingTurret { get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); } @@ -82,64 +79,40 @@ namespace Barotrauma } private float skillIncreasePerFriendlyHealed; - [Serialize(0.001f, true)] + [Serialize(0.001f, IsPropertySaveable.Yes)] public float SkillIncreasePerFriendlyHealed { get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); } set { skillIncreasePerFriendlyHealed = value; } } - [Serialize(1.1f, true)] + [Serialize(1.1f, IsPropertySaveable.Yes)] public float AssistantSkillIncreaseMultiplier { get; set; } - [Serialize(200.0f, true)] + [Serialize(200.0f, IsPropertySaveable.Yes)] public float MaximumSkillWithTalents { get; set; } - private SkillSettings(XElement element) + public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier()) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } public string Name => "SkillSettings"; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; } - public static void Load(IEnumerable files) - { - //reverse order to respect content package load order (last file overrides others) - foreach (ContentFile file in files.Reverse()) - { - if (file.Type != ContentType.SkillSettings) - { - throw new ArgumentException(); - } - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - - Current = new SkillSettings(doc.Root); - break; - } - - if (Current == null) - { - DebugConsole.NewMessage("No skill settings found in the selected content packages. Using default values."); - Current = new SkillSettings(null); - } - } - private float GetCurrentSkillGainMultiplier() { if (GameMain.GameSession?.GameMode is CampaignMode) @@ -151,5 +124,7 @@ namespace Barotrauma return SingleRoundSkillGainMultiplier; } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index 959cf4148..1b3564d0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Abilities public virtual bool AllowClientSimulation => true; - public AbilityCondition(CharacterTalent characterTalent, XElement conditionElement) + public AbilityCondition(CharacterTalent characterTalent, ContentXElement conditionElement) { this.characterTalent = characterTalent; character = characterTalent.Character; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs index 4d909aa81..ebd077561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities class AbilityConditionAffliction : AbilityConditionData { private readonly string[] afflictions; - public AbilityConditionAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index b45a81b25..950684465 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -1,5 +1,5 @@ -using Barotrauma.Items.Components; using System; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -25,10 +25,10 @@ namespace Barotrauma.Abilities private readonly string[] tags; private readonly WeaponType weapontype; private readonly bool ignoreNonHarmfulAttacks; - public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAttackData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty); - tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false); string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); @@ -54,7 +54,7 @@ namespace Barotrauma.Abilities if (!string.IsNullOrEmpty(itemIdentifier)) { - if (item?.prefab.Identifier != itemIdentifier) + if (item?.Prefab.Identifier != itemIdentifier) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs index 58616eac5..b0a9864ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using System; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -8,11 +9,11 @@ namespace Barotrauma.Abilities class AbilityConditionAttackResult : AbilityConditionData { private readonly List targetTypes; - private readonly string[] afflictions; - public AbilityConditionAttackResult(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + private readonly Identifier[] afflictions; + public AbilityConditionAttackResult(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); - afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty())); + afflictions = conditionElement.GetAttributeIdentifierArray("afflictions", Array.Empty()); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index cc14b466a..43a16839d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -10,9 +10,9 @@ namespace Barotrauma.Abilities private List conditionals = new List(); - public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)); foreach (XElement subElement in conditionElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs index 0d25f107e..7065cb683 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Abilities /// /// These conditions will return an error if used outside their limited intended use. /// - public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs index 2e1204fba..12538c312 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Abilities { class AbilityConditionEvasiveManeuvers : AbilityConditionData { - public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs index 6ea6dd5e9..b72555ccf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities class AbilityConditionGeneHarvester : AbilityConditionData { - public AbilityConditionGeneHarvester(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionGeneHarvester(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs index 26a04a1a7..957779bb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Abilities private readonly bool hittingCountsAsAiming; private readonly WeaponType weapontype; - public AbilityConditionIsAiming(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionIsAiming(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { hittingCountsAsAiming = conditionElement.GetAttributeBool("hittingcountsasaiming", false); switch (conditionElement.GetAttributeString("weapontype", "")) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index d4eb985d2..5811b3d66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities private readonly string[] identifiers; private readonly string[] tags; - public AbilityConditionItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty(), convertToLowerInvariant: true); tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs index 330bcfcd2..9bbb48bf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { private readonly SubmarineType? submarineType; - public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { if (conditionElement.Attribute("submarinetype") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index b2d70b0b3..59c8254b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Abilities @@ -6,15 +7,15 @@ namespace Barotrauma.Abilities class AbilityConditionLocation : AbilityConditionData { private readonly bool? hasOutpost; - private readonly string[] locationIdentifiers; + private readonly Identifier[] locationIdentifiers; - public AbilityConditionLocation(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { if (conditionElement.Attribute("hasoutpost") != null) { hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); } - locationIdentifiers = conditionElement.GetAttributeStringArray("locationtype", new string[0]); + locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty()); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index f7f0ffed4..0e19ec19e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class AbilityConditionMission : AbilityConditionData { private readonly MissionType missionType; - public AbilityConditionMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { string missionTypeString = conditionElement.GetAttributeString("missiontype", "None"); if (!Enum.TryParse(missionTypeString, out missionType)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs index 1068da08b..d3ece3bf8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -1,4 +1,5 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -7,9 +8,9 @@ namespace Barotrauma.Abilities private readonly string[] allowedTypes; private readonly string identifier; - public AbilityConditionReduceAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionReduceAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", new string[0], convertToLowerInvariant: true); + allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", Array.Empty(), convertToLowerInvariant: true); identifier = conditionElement.GetAttributeString("identifier", ""); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs index 52d189213..eb4be84c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs @@ -6,19 +6,19 @@ namespace Barotrauma.Abilities { private readonly string skillIdentifier; - public AbilityConditionSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); } - private bool MatchesConditionSpecific(string skillIdentifier) + private bool MatchesConditionSpecific(Identifier skillIdentifier) { return this.skillIdentifier == skillIdentifier; } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) + if (abilityObject is IAbilitySkillIdentifier { SkillIdentifier: Identifier skillIdentifier }) { return MatchesConditionSpecific(skillIdentifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs index 6be5969f8..3effceb4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private string effectIdentifier; - public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { effectIdentifier = conditionElement.GetAttributeString("effectidentifier", "").ToLowerInvariant(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs index 9fe8fa1a4..de5597011 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { private readonly float vitalityPercentage; - public AbilityConditionAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAboveVitality(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs index 29256ab7c..55a640a17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { float vitalityPercentage; - public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs index 7525427eb..50c650f1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private readonly string jobIdentifier; - public AbilityConditionCoauthor(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionCoauthor(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { jobIdentifier = conditionElement.GetAttributeString("jobidentifier", string.Empty); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs index cd96edb58..cc44ec6ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities class AbilityConditionCrouched : AbilityConditionDataless { - public AbilityConditionCrouched(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionCrouched(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs index ad7007fd6..a1e03fcb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Abilities { abstract class AbilityConditionDataless : AbilityCondition { - public AbilityConditionDataless(CharacterTalent characterTalent, XElement conditionElement) : base (characterTalent, conditionElement) { } + public AbilityConditionDataless(CharacterTalent characterTalent, ContentXElement conditionElement) : base (characterTalent, conditionElement) { } protected abstract bool MatchesConditionSpecific(); public override bool MatchesCondition() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs index 9f449e43c..68c46cd12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities private float minimumPercentage; - public AbilityConditionHasAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { afflictionIdentifier = conditionElement.GetAttributeString("afflictionidentifier", ""); minimumPercentage = conditionElement.GetAttributeFloat("minimumpercentage", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs index 0f1707d3d..fc3e186c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class AbilityConditionHasDifferentJobs : AbilityConditionDataless { private readonly int amount; - public AbilityConditionHasDifferentJobs(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasDifferentJobs(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { amount = conditionElement.GetAttributeInt("amount", 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 8f4fc7c35..4407fcb18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using System; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -14,9 +15,9 @@ namespace Barotrauma.Abilities private List items = new List(); - public AbilityConditionHasItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); requireAll = conditionElement.GetAttributeBool("requireall", false); //this.invSlotType = invSlotType; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 2c3b26a5c..344a580f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -5,14 +5,14 @@ namespace Barotrauma.Abilities { class AbilityConditionHasPermanentStat : AbilityConditionDataless { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; private readonly StatTypes statType; private readonly float min; - public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - statIdentifier = conditionElement.GetAttributeString("statidentifier", string.Empty); - if (string.IsNullOrEmpty(statIdentifier)) + statIdentifier = conditionElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); + if (statIdentifier.IsEmpty) { DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs index 60d5da1f7..865384e7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private readonly string skillIdentifier; private readonly float minValue; - public AbilityConditionHasSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty); minValue = conditionElement.GetAttributeFloat("minvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs index 2a22f2098..1b8d54fc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities private readonly string tag; - public AbilityConditionHasStatusTag(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasStatusTag(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { tag = conditionElement.GetAttributeString("tag", ""); if (string.IsNullOrEmpty(tag)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs index d3aa75bde..d54fd0839 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private readonly float velocity; - public AbilityConditionHasVelocity(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasVelocity(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { velocity = conditionElement.GetAttributeFloat("velocity", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs index 28f27ed9b..4bda9d8ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionInFriendlySubmarine : AbilityConditionDataless { - public AbilityConditionInFriendlySubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionInFriendlySubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs index e08291e6b..484e0c7bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionInHull : AbilityConditionDataless { - public AbilityConditionInHull(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionInHull(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs index d93731514..a6191d470 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionInWater : AbilityConditionDataless { - public AbilityConditionInWater(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionInWater(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs index f2c4b2fb7..46e55c575 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class AbilityConditionLevelsBehindHighest : AbilityConditionDataless { private readonly int levelsBehind; - public AbilityConditionLevelsBehindHighest(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionLevelsBehindHighest(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { levelsBehind = conditionElement.GetAttributeInt("levelsbehind", 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs index bb4390106..177cd4bf2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { class AbilityConditionNoCrewDied : AbilityConditionDataless { - public AbilityConditionNoCrewDied(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionNoCrewDied(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs index dac9a3f1a..ac5941a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionOnMission : AbilityConditionDataless { - public AbilityConditionOnMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionOnMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs index 192ea6f4f..3cc7c0eac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities class AbilityConditionRagdolled : AbilityConditionDataless { - public AbilityConditionRagdolled(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionRagdolled(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs index 3186b852f..78bbe3f68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionRunning : AbilityConditionDataless { - public AbilityConditionRunning(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionRunning(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs index 5b582f799..b774712ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly float randomChance = 0f; public override bool AllowClientSimulation => false; - public AbilityConditionServerRandom(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionServerRandom(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { randomChance = conditionElement.GetAttributeFloat("randomchance", 1f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs index 9a99f4ce8..f75f98a89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities class AbilityConditionShipFlooded : AbilityConditionDataless { private readonly float floodPercentage; - public AbilityConditionShipFlooded(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionShipFlooded(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { floodPercentage = conditionElement.GetAttributeFloat("floodpercentage", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs index ef57527d6..1d6c431e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs @@ -32,7 +32,7 @@ interface IAbilitySkillIdentifier { - public string SkillIdentifier { get; set; } + public Identifier SkillIdentifier { get; set; } } interface IAbilityAffliction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 6d7038f4c..9d1e093b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -15,5 +15,5 @@ namespace Barotrauma.Abilities } public Character Character { get; set; } } - + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 27d856553..4c29a5b61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Abilities /// protected float EffectDeltaTime => CharacterAbilityGroup is CharacterAbilityGroupInterval abilityGroupInterval ? abilityGroupInterval.TimeSinceLastUpdate : DefaultEffectTime; - public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) + public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) { CharacterAbilityGroup = characterAbilityGroup; CharacterTalent = characterAbilityGroup.CharacterTalent; @@ -93,7 +93,7 @@ namespace Barotrauma.Abilities } // XML - public static CharacterAbility Load(XElement abilityElement, CharacterAbilityGroup characterAbilityGroup, bool errorMessages = true) + public static CharacterAbility Load(ContentXElement abilityElement, CharacterAbilityGroup characterAbilityGroup, bool errorMessages = true) { Type abilityType; string type = abilityElement.Name.ToString().ToLowerInvariant(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs index 7dde00097..e934eb2aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Abilities private readonly HashSet limbTypes = new HashSet(); public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { force = abilityElement.GetAttributeFloat("force", 0f); maxVelocity = abilityElement.GetAttributeFloat("maxvelocity", 10f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index c30ac8152..70ec6e1ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities readonly List targets = new List(); - public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); applyToSelf = abilityElement.GetAttributeBool("applytoself", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index a2fc33e5c..3f9090376 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private readonly bool allowSelf; private readonly float maxDistance = float.MaxValue; - public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { allowSelf = abilityElement.GetAttributeBool("allowself", true); maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs index efca07622..d6fc8b329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects { - public CharacterAbilityApplyStatusEffectsToAttacker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToAttacker(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs index fc4291453..4594c5e1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects { - public CharacterAbilityApplyStatusEffectsToLastOrderedCharacter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToLastOrderedCharacter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs index a0701c782..f8329aae2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class CharacterAbilityApplyStatusEffectsToNearestAlly : CharacterAbilityApplyStatusEffects { protected float squaredMaxDistance; - public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs index 0f1fd20b2..c49c4b266 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs @@ -1,9 +1,7 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -15,7 +13,7 @@ namespace Barotrauma.Abilities public override bool AllowClientSimulation => false; - public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); allowDifferentSub = abilityElement.GetAttributeBool("mustbeonsamesub", true); @@ -24,18 +22,33 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - Character chosenCharacter = null; + ApplyEffect(Character); + } - chosenCharacter = Character.GetFriendlyCrew(Character).Where(c => - (allowSelf || c != Character) && + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter) + { + ApplyEffect(targetCharacter); + } + else + { + ApplyEffect(Character); + } + } + + private void ApplyEffect(Character thisCharacter) + { + Character chosenCharacter = + Character.GetFriendlyCrew(thisCharacter).Where(c => + (allowSelf || c != thisCharacter) && (allowDifferentSub || c.Submarine == Character.Submarine) && - Vector2.DistanceSquared(Character.WorldPosition, c.WorldPosition) is float tempDistance && - tempDistance < squaredMaxDistance).GetRandom(); - + Vector2.DistanceSquared(thisCharacter.WorldPosition, c.WorldPosition) is float tempDistance && + tempDistance < squaredMaxDistance).GetRandomUnsynced(); if (chosenCharacter == null) { return; } ApplyEffectSpecific(chosenCharacter); - } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 9f37a0e03..1204769de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -5,12 +5,12 @@ namespace Barotrauma.Abilities { class CharacterAbilityGainSimultaneousSkill : CharacterAbility { - private readonly string skillIdentifier; + private readonly Identifier skillIdentifier; private readonly bool ignoreAbilitySkillGain; - public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 5f56b433a..9dcab6bb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -4,19 +4,19 @@ namespace Barotrauma.Abilities { class CharacterAbilityGiveAffliction : CharacterAbility { - private readonly string afflictionId; + private readonly Identifier afflictionId; private readonly float strength; private readonly string multiplyStrengthBySkill; private readonly bool setValue; - public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - afflictionId = abilityElement.GetAttributeString("afflictionid", abilityElement.GetAttributeString("affliction", string.Empty)); + afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty)); strength = abilityElement.GetAttributeFloat("strength", 0f); multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty); setValue = abilityElement.GetAttributeBool("setvalue", false); - if (string.IsNullOrEmpty(afflictionId)) + if (afflictionId.IsEmpty) { DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set."); } @@ -26,7 +26,7 @@ namespace Barotrauma.Abilities { if (abilityObject is IAbilityCharacter character) { - var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier.Equals(afflictionId, System.StringComparison.OrdinalIgnoreCase)); + var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId); if (afflictionPrefab == null) { DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\"."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs index 76b3960ea..e56bee86c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly AbilityFlags abilityFlag; // this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types - public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index 11aa9934e..40148524c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -7,25 +7,25 @@ namespace Barotrauma.Abilities public override bool AppliesEffectOnIntervalUpdate => true; private readonly int amount; - private readonly string scalingStatIdentifier; + private readonly Identifier scalingStatIdentifier; - public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { amount = abilityElement.GetAttributeInt("amount", 0); - scalingStatIdentifier = abilityElement.GetAttributeString("scalingstatidentifier", string.Empty); + scalingStatIdentifier = abilityElement.GetAttributeIdentifier("scalingstatidentifier", Identifier.Empty); } private void ApplyEffectSpecific(Character targetCharacter) { float multiplier = 1f; - if (!string.IsNullOrEmpty(scalingStatIdentifier)) + if (!scalingStatIdentifier.IsEmpty) { multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier); } int totalAmount = (int)(multiplier * amount); targetCharacter.GiveMoney(totalAmount); - GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index 0998bf475..a0750d5d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities public override bool AllowClientSimulation => true; public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 253dd787b..347f69a25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -4,15 +4,15 @@ namespace Barotrauma.Abilities { class CharacterAbilityGiveResistance : CharacterAbility { - private readonly string resistanceId; + private readonly Identifier resistanceId; private readonly float multiplier; - public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - resistanceId = abilityElement.GetAttributeString("resistanceid", abilityElement.GetAttributeString("resistance", string.Empty)); + resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty)); multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency - if (string.IsNullOrEmpty(resistanceId)) + if (resistanceId.IsEmpty) { DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs index c999d3999..a97ec2ee4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly StatTypes statType; private readonly float value; - public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs index 8ba1c9ef9..1eed1afae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private readonly int amount; - public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { amount = abilityElement.GetAttributeInt("amount", 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs index 7a53e6d91..966bac5f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -8,15 +8,15 @@ namespace Barotrauma.Abilities { public override bool AppliesEffectOnIntervalUpdate => true; - private readonly string skillIdentifier; + private readonly Identifier skillIdentifier; private readonly float skillIncrease; - public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); skillIncrease = abilityElement.GetAttributeFloat("skillincrease", 0f); - if (string.IsNullOrEmpty(skillIdentifier)) + if (skillIdentifier.IsEmpty) { DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill."); } @@ -45,9 +45,9 @@ namespace Barotrauma.Abilities private void ApplyEffectSpecific(Character character) { - if (skillIdentifier.Equals("random")) + if (skillIdentifier == "random") { - var skill = character.Info?.Job?.Skills?.GetRandom(); + var skill = character.Info?.Job?.Skills?.GetRandomUnsynced(); if (skill == null) { return; } character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease, gainedFromAbility: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index e3896090c..97b0302bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities private readonly float addedMultiplier; - public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true); addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index 9b4700fe5..a6141b79f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -11,9 +11,9 @@ namespace Barotrauma.Abilities private readonly float addedPenetration; private readonly bool implode; - public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - if (abilityElement.GetChildElement("afflictions") is XElement afflictionElements) + if (abilityElement.GetChildElement("afflictions") is ContentXElement afflictionElements) { afflictions = CharacterAbilityGroup.ParseAfflictions(CharacterTalent, afflictionElements); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs index 8993f0ccd..52f47c471 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private bool lastState; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs index 010f04f2f..ed5a5a35f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -4,18 +4,18 @@ namespace Barotrauma.Abilities { class CharacterAbilityModifyResistance : CharacterAbility { - private readonly string resistanceId; + private readonly Identifier resistanceId; private readonly float resistance; bool lastState; public override bool AllowClientSimulation => true; // should probably be split to different classes - public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - resistanceId = abilityElement.GetAttributeString("resistanceid", ""); + resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", ""); resistance = abilityElement.GetAttributeFloat("resistance", 1f); - if (string.IsNullOrEmpty(resistanceId)) + if (resistanceId.IsEmpty) { DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs index 74e04a098..c7f792475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities bool lastState; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs index 3269e078a..29d1fdf3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStatToFlooding(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStatToFlooding(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs index 14ac0324c..a3141b037 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStatToLevel(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStatToLevel(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); statPerLevel = abilityElement.GetAttributeFloat("statperlevel", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs index b2a01a4c1..3311a5ff6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStatToSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStatToSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs index 59a203a7f..6970c2e6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly float addedValue; private readonly float multiplyValue; - public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs index 3d280aeeb..10c1ddcd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -4,14 +4,14 @@ namespace Barotrauma.Abilities { class CharacterAbilityPutItem : CharacterAbility { - private readonly string itemIdentifier; + private readonly Identifier itemIdentifier; private readonly int amount; public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityPutItem(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityPutItem(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - itemIdentifier = abilityElement.GetAttributeString("itemidentifier", ""); + itemIdentifier = abilityElement.GetAttributeIdentifier("itemidentifier", ""); amount = abilityElement.GetAttributeInt("amount", 1); - if (string.IsNullOrEmpty(itemIdentifier)) + if (itemIdentifier.IsEmpty) { DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined."); } @@ -19,7 +19,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - if (string.IsNullOrEmpty(itemIdentifier)) + if (itemIdentifier.IsEmpty) { DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined."); return; @@ -46,7 +46,7 @@ namespace Barotrauma.Abilities } else { - Entity.Spawner.AddToSpawnQueue(itemPrefab, Character.Inventory); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, Character.Inventory); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index 3716a6fbc..1660c54e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities public override bool AppliesEffectOnIntervalUpdate => true; public override bool AllowClientSimulation => true; - public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs index 7ed61e90f..21d679bd2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs index 919848c3a..4d6647f55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Abilities private readonly float randomChance; private readonly bool oncePerContainer; - public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs index 8a88acea6..c76b7fb01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs @@ -6,32 +6,31 @@ namespace Barotrauma.Abilities { class CharacterAbilityUnlockTree : CharacterAbility { - public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } public override void InitializeAbility(bool addingFirstTime) { - if (!addingFirstTime) { return; } - if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab))); + if (subTree == null) { return; } + + subTree.ForceUnlock = true; + if (!addingFirstTime) { return; } - if (subTree != null) + foreach (var talentOption in subTree.TalentOptionStages) { - subTree.ForceUnlock = true; - foreach (var talentOption in subTree.TalentOptionStages) + foreach (var talent in talentOption.Talents) { - foreach (var talent in talentOption.Talents) + if (talent == CharacterTalent.Prefab) { continue; } + if (Character.GiveTalent(talent)) { - if (talent == CharacterTalent.Prefab) { continue; } - if (Character.GiveTalent(talent)) - { - Character.Info.AdditionalTalentPoints++; - } + Character.Info.AdditionalTalentPoints++; } } - } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs index a6907ca41..9a67d1fd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Abilities private readonly float maxAddedDamageMultiplier; private readonly string[] tags; - public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedDamageMultiplierPerItem = abilityElement.GetAttributeFloat("addeddamagemultiplierperitem", 0f); maxAddedDamageMultiplier = abilityElement.GetAttributeFloat("maxaddedddamagemultiplier", float.MaxValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index 2cc7a26f1..e212b3224 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { private readonly bool ignoreAbilitySkillGain; - public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs index 5ab9361b8..745cb706e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs @@ -8,14 +8,14 @@ namespace Barotrauma.Abilities { private readonly float addedValue; private readonly float multiplyValue; - private readonly string[] tags; + private readonly Identifier[] tags; private readonly int maxMultiplyCount; - - public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + + public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); - tags = abilityElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + tags = abilityElement.GetAttributeIdentifierArray("tags", Array.Empty()); maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs index 00c39c0e0..75aa86835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private float vitalityPercentage; - public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { vitalityPercentage = abilityElement.GetAttributeFloat("vitalitypercentage", 0f); } @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities { int totalAmount = (int)(vitalityPercentage * character.MaxVitality); Character.GiveMoney(totalAmount); - GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index 66a39efaa..c3c99c080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private readonly int experienceAmount; private readonly int max; - public CharacterAbilityByTheBook(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityByTheBook(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { moneyAmount = abilityElement.GetAttributeInt("moneyamount", 0); experienceAmount = abilityElement.GetAttributeInt("experienceamount", 0); @@ -30,7 +30,7 @@ namespace Barotrauma.Abilities if (!enemyCharacter.LockHands) { continue; } if (timesGiven > max) { continue; } Character.GiveMoney(moneyAmount); - GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); foreach (Character character in Character.GetFriendlyCrew(Character)) { character.Info?.GiveExperience(experienceAmount); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs index fe4073afc..3ea78fd56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Abilities private readonly int moneyPerMission; - public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0); } @@ -23,7 +23,7 @@ namespace Barotrauma.Abilities { int totalAmount = moneyPerMission * info.MissionsCompletedSinceDeath; Character.GiveMoney(totalAmount); - GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs index c66630989..24b2a02e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -5,15 +5,15 @@ namespace Barotrauma.Abilities { class CharacterAbilityMultitasker : CharacterAbility { - private string lastSkillIdentifier; + private Identifier lastSkillIdentifier; - public CharacterAbilityMultitasker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityMultitasker(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) + if (abilityObject is IAbilitySkillIdentifier { SkillIdentifier: Identifier skillIdentifier }) { if (skillIdentifier != lastSkillIdentifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs index 0f49b2e56..c9a160923 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs index 574d9d7b1..85ef28d54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Abilities // seems like a minor issue for now private readonly List openedContainers = new List(); - public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index 2fbb95ec9..5bcac5e83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities { // this should just be its own class, misleading to inherit here private readonly string tag; - public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { tag = abilityElement.GetAttributeString("tag", ""); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 7c96b3d17..1f3795dea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -26,13 +26,13 @@ namespace Barotrauma.Abilities // separate dictionaries for each type of characterability? protected readonly List characterAbilities = new List(); - public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) + public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) { AbilityEffectType = abilityEffectType; CharacterTalent = characterTalent; Character = CharacterTalent.Character; maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue); - foreach (XElement subElement in abilityElementGroup.Elements()) + foreach (var subElement in abilityElementGroup.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -54,9 +54,9 @@ namespace Barotrauma.Abilities } } - public void LoadConditions(XElement conditionElements) + public void LoadConditions(ContentXElement conditionElements) { - foreach (XElement conditionElement in conditionElements.Elements()) + foreach (ContentXElement conditionElement in conditionElements.Elements()) { AbilityCondition newCondition = ConstructCondition(CharacterTalent, conditionElement); @@ -87,7 +87,7 @@ namespace Barotrauma.Abilities } // XML - private AbilityCondition ConstructCondition(CharacterTalent characterTalent, XElement conditionElement, bool errorMessages = true) + private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true) { AbilityCondition newCondition = null; @@ -129,15 +129,15 @@ namespace Barotrauma.Abilities return newCondition; } - private void LoadAbilities(XElement abilityElements) + private void LoadAbilities(ContentXElement abilityElements) { - foreach (XElement abilityElementGroup in abilityElements.Elements()) + foreach (var abilityElementGroup in abilityElements.Elements()) { AddAbility(ConstructAbility(abilityElementGroup, CharacterTalent)); } } - private CharacterAbility ConstructAbility(XElement abilityElement, CharacterTalent characterTalent) + private CharacterAbility ConstructAbility(ContentXElement abilityElement, CharacterTalent characterTalent) { CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this); @@ -150,7 +150,7 @@ namespace Barotrauma.Abilities return newAbility; } - public static List ParseStatusEffects(CharacterTalent characterTalent, XElement statusEffectElements) + public static List ParseStatusEffects(CharacterTalent characterTalent, ContentXElement statusEffectElements) { if (statusEffectElements == null) { @@ -160,7 +160,7 @@ namespace Barotrauma.Abilities List statusEffects = new List(); - foreach (XElement statusEffectElement in statusEffectElements.Elements()) + foreach (var statusEffectElement in statusEffectElements.Elements()) { var statusEffect = StatusEffect.Load(statusEffectElement, characterTalent.DebugIdentifier); statusEffects.Add(statusEffect); @@ -178,7 +178,7 @@ namespace Barotrauma.Abilities return statType; } - public static List ParseAfflictions(CharacterTalent characterTalent, XElement afflictionElements) + public static List ParseAfflictions(CharacterTalent characterTalent, ContentXElement afflictionElements) { if (afflictionElements == null) { @@ -191,10 +191,10 @@ namespace Barotrauma.Abilities // similar logic to affliction creation in statuseffects // might be worth unifying - foreach (XElement afflictionElement in afflictionElements.Elements()) + foreach (var afflictionElement in afflictionElements.Elements()) { - string afflictionIdentifier = afflictionElement.GetAttributeString("identifier", "").ToLowerInvariant(); - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + Identifier afflictionIdentifier = afflictionElement.GetAttributeIdentifier("identifier", ""); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index e4d488103..a12e2ce1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { - public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) : + public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) { } public void CheckAbilityGroup(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index c7a302149..8682a47df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Abilities private float effectDelayTimer; - public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) : + public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) { // too many overlapping intervals could cause hitching? maybe randomize a little diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 3a79e1b2a..bfa5b6869 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -20,17 +20,17 @@ namespace Barotrauma private readonly List characterAbilityGroupIntervals = new List(); // works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well - public List UnlockedRecipes { get; } = new List(); + public List UnlockedRecipes { get; } = new List(); public CharacterTalent(TalentPrefab talentPrefab, Character character) { Character = character; Prefab = talentPrefab; - XElement element = talentPrefab.ConfigElement; + var element = talentPrefab.ConfigElement; DebugIdentifier = talentPrefab.OriginalName; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -41,7 +41,7 @@ namespace Barotrauma LoadAbilityGroupInterval(subElement); break; case "addedrecipe": - if (subElement.GetAttributeString("itemidentifier", string.Empty) is string recipeIdentifier && recipeIdentifier != string.Empty) + if (subElement.GetAttributeIdentifier("itemidentifier", Identifier.Empty) is { IsEmpty: false } recipeIdentifier) { UnlockedRecipes.Add(recipeIdentifier); } @@ -85,12 +85,12 @@ namespace Barotrauma } // XML logic - private void LoadAbilityGroupInterval(XElement abilityGroup) + private void LoadAbilityGroupInterval(ContentXElement abilityGroup) { characterAbilityGroupIntervals.Add(new CharacterAbilityGroupInterval(AbilityEffectType.Undefined, this, abilityGroup)); } - private void LoadAbilityGroupEffect(XElement abilityGroup) + private void LoadAbilityGroupEffect(ContentXElement abilityGroup) { AbilityEffectType abilityEffectType = ParseAbilityEffectType(this, abilityGroup.GetAttributeString("abilityeffecttype", "none")); AddAbilityGroupEffect(new CharacterAbilityGroupEffect(abilityEffectType, this, abilityGroup), abilityEffectType); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 492b857d9..ad164626d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -4,35 +4,33 @@ using System.Xml.Linq; namespace Barotrauma { - class TalentPrefab : IPrefab, IDisposable, IHasUintIdentifier + class TalentPrefab : PrefabWithUintIdentifier { - public string Identifier { get; private set; } - public string OriginalName => Identifier; - public ContentPackage ContentPackage { get; private set; } - public string FilePath { get; private set; } + public string OriginalName => Identifier.Value; - public string DisplayName { get; private set; } + public LocalizedString DisplayName { get; private set; } - public string Description { get; private set; } + public LocalizedString Description { get; private set; } public readonly Sprite Icon; public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); - public XElement ConfigElement + public ContentXElement ConfigElement { get; private set; } - public TalentPrefab(XElement element, string filePath) + public TalentPrefab(ContentXElement element, TalentsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) { - FilePath = filePath; ConfigElement = element; - Identifier = element.GetAttributeString("identifier", "noidentifier"); - DisplayName = TextManager.Get("talentname." + Identifier, returnNull: true) ?? Identifier; - foreach (XElement subElement in element.Elements()) + DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value); + + Description = ""; + + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -40,112 +38,29 @@ namespace Barotrauma Icon = new Sprite(subElement); break; case "description": - string tempDescription = Description; + var tempDescription = Description; TextManager.ConstructDescription(ref tempDescription, subElement); Description = tempDescription; break; } } - if (string.IsNullOrEmpty(Description)) + if (element.Attribute("description") != null) { - if (element.Attribute("description") != null) - { - string description = element.GetAttributeString("description", string.Empty); - Description = TextManager.Get(description, returnNull: true) ?? description; - } - else - { - Description = TextManager.Get("talentdescription." + Identifier, returnNull: true) ?? string.Empty; - } + string description = element.GetAttributeString("description", string.Empty); + Description = Description.Fallback(TextManager.Get(description)).Fallback(description); } - -#if DEBUG - if (!TextManager.ContainsTag("talentname." + Identifier)) + else { - DebugConsole.AddWarning($"Name for the talent \"{Identifier}\" not found in the text files."); + Description = Description.Fallback(TextManager.Get($"talentdescription.{Identifier}")).Fallback(string.Empty); } - if (string.IsNullOrEmpty(Description)) - { - DebugConsole.AddWarning($"Description for the talent \"{Identifier}\" not configured"); - } - if (Description.Contains('[')) - { - DebugConsole.ThrowError($"Description for the talent \"{Identifier}\" contains brackets - was some variable not replaced correctly? ({Description})"); - } -#endif } private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; - TalentPrefabs.Remove(this); - } - - /// - /// Unique identifier that's generated by hashing the prefab's string identifier. - /// Used to reduce the amount of bytes needed to write talent data into network messages in multiplayer. - /// - public uint UIntIdentifier { get; set; } - - public static void RemoveByFile(string filePath) => TalentPrefabs.RemoveByFile(filePath); - - public static void LoadFromFile(ContentFile file) - { - DebugConsole.Log("Loading talent prefab: " + file.Path); - RemoveByFile(file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - void loadSinglePrefab(XElement element, bool isOverride) - { - var newPrefab = new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage }; - TalentPrefabs.Add(newPrefab, isOverride); - newPrefab.CalculatePrefabUIntIdentifier(TalentPrefabs); - } - - void loadMultiplePrefabs(XElement element, bool isOverride) - { - foreach (var subElement in element.Elements()) - { - interpretElement(subElement, isOverride); - } - } - - void interpretElement(XElement subElement, bool isOverride) - { - if (subElement.IsOverride()) - { - loadMultiplePrefabs(subElement, true); - } - else if (subElement.Name.LocalName.Equals("talents", StringComparison.OrdinalIgnoreCase)) - { - loadMultiplePrefabs(subElement, isOverride); - } - else if (subElement.Name.LocalName.Equals("talent", StringComparison.OrdinalIgnoreCase)) - { - loadSinglePrefab(subElement, isOverride); - } - else - { - DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentPrefab)} prefab type: '{subElement.Name}' in {file.Path}"); - } - } - - interpretElement(doc.Root, false); - } - - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading talent prefabs: "); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 20f7865ae..0d6c08118 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - class TalentTree : IPrefab, IDisposable + class TalentTree : Prefab { public enum TalentTreeStageState { @@ -20,124 +21,44 @@ namespace Barotrauma public readonly List TalentSubTrees = new List(); - public XElement ConfigElement + public ContentXElement ConfigElement { get; private set; } - public string OriginalName => Identifier; - - public string Identifier { get; } - - public string FilePath { get; } - - public ContentPackage ContentPackage { get; set; } - - public TalentTree(XElement element, string filePath) + public TalentTree(ContentXElement element, TalentTreesFile file) : base(file, element.GetAttributeIdentifier("jobIdentifier", "")) { ConfigElement = element; - FilePath = filePath; - Identifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant(); - if (string.IsNullOrEmpty(Identifier)) + if (Identifier.IsEmpty) { - DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!"); + DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); return; } - foreach (XElement subTreeElement in element.GetChildElements("subtree")) + foreach (var subTreeElement in element.GetChildElements("subtree")) { TalentSubTrees.Add(new TalentSubTree(subTreeElement)); } - - // talents found and unlocked using the identifier wihin the talent tree, so no duplicates may occur - HashSet duplicateSet = new HashSet(); - foreach (string talent in TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier)))) - { - TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase)); - if (talentPrefab == null) - { - DebugConsole.AddWarning($"Talent tree for job {Identifier} contains non-existent talent {talent}! Talent tree not added."); - return; - } - if (!duplicateSet.Add(talent)) - { - DebugConsole.ThrowError($"Talent tree for job {Identifier} contains duplicate talent {talent}! Talent tree not added."); - return; - } - } } - - public bool TalentIsInTree(string talentIdentifier) + + public bool TalentIsInTree(Identifier talentIdentifier) { return TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))).Any(c => c == talentIdentifier); } - public static void LoadFromFile(ContentFile file) + public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier) { - DebugConsole.Log("Loading talent tree: " + file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - void loadSinglePrefab(XElement element, bool isOverride) - { - JobTalentTrees.Add(new TalentTree(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride); - } - - void loadMultiplePrefabs(XElement element, bool isOverride) - { - foreach (var subElement in element.Elements()) - { - interpretElement(subElement, isOverride); - } - } - - void interpretElement(XElement subElement, bool isOverride) - { - if (subElement.IsOverride()) - { - loadMultiplePrefabs(subElement, true); - } - else if (subElement.Name.LocalName.Equals("talenttrees", StringComparison.OrdinalIgnoreCase)) - { - loadMultiplePrefabs(subElement, isOverride); - } - else if (subElement.Name.LocalName.Equals("talenttree", StringComparison.OrdinalIgnoreCase)) - { - loadSinglePrefab(subElement, isOverride); - } - else - { - DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentTree)} prefab type: '{subElement.Name}' in {file.Path}"); - } - } - - interpretElement(doc.Root, false); - } - - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading talent tree: "); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - } - - public static bool IsViableTalentForCharacter(Character character, string talentIdentifier) - { - return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? Enumerable.Empty()); + return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (ICollection)Array.Empty()); } // i hate this function - markus - public static TalentTreeStageState GetTalentOptionStageState(Character character, string subTreeIdentifier, int index, List selectedTalents) + public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List selectedTalents) { if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } - if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } + if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); @@ -187,12 +108,12 @@ namespace Barotrauma } - public static bool IsViableTalentForCharacter(Character character, string talentIdentifier, IEnumerable selectedTalents) + public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, ICollection selectedTalents) { if (character?.Info?.Job.Prefab == null) { return false; } if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } - if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } + if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } foreach (var subTree in talentTree.TalentSubTrees) { @@ -218,15 +139,15 @@ namespace Barotrauma return false; } - public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) + public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) { - List viableTalents = new List(); + List viableTalents = new List(); bool canStillUnlock = true; // keep trying to unlock talents until none of the talents are unlockable while (canStillUnlock && selectedTalents.Any()) { canStillUnlock = false; - foreach (string talent in selectedTalents) + foreach (Identifier talent in selectedTalents) { if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents)) { @@ -238,32 +159,26 @@ namespace Barotrauma return viableTalents; } - private bool disposed = false; - public void Dispose() - { - if (disposed) { return; } - disposed = true; - JobTalentTrees.Remove(this); - } + public override void Dispose() { } } class TalentSubTree { - public string Identifier { get; } + public Identifier Identifier { get; } - public string DisplayName { get; } + public LocalizedString DisplayName { get; } public bool ForceUnlock; public readonly List TalentOptionStages = new List(); - public TalentSubTree(XElement subTreeElement) + public TalentSubTree(ContentXElement subTreeElement) { - Identifier = subTreeElement.GetAttributeString("identifier", ""); + Identifier = subTreeElement.GetAttributeIdentifier("identifier", ""); - DisplayName = TextManager.Get("talenttree." + Identifier, returnNull: true) ?? Identifier; + DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value); - foreach (XElement talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) + foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) { TalentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); } @@ -273,21 +188,20 @@ namespace Barotrauma class TalentOption { - public readonly List Talents = new List(); + private readonly ImmutableHashSet talentIdentifiers; - public TalentOption(XElement talentOptionsElement, string debugIdentifier) + public IEnumerable Talents + => talentIdentifiers.Select(id => TalentPrefab.TalentPrefabs[id]); + + public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier) { - foreach (XElement talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) + var talentIdentifiers = new HashSet(); + foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) { - string identifier = talentOptionElement.GetAttributeString("identifier", string.Empty); - - if (!TalentPrefab.TalentPrefabs.ContainsKey(identifier)) - { - DebugConsole.ThrowError($"Error in talent tree \"{debugIdentifier}\" - could not find a talent with the identifier \"{identifier}\"."); - return; - } - Talents.Add(TalentPrefab.TalentPrefabs[identifier]); + Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + talentIdentifiers.Add(identifier); } + this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs new file mode 100644 index 000000000..e3cfc518c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs @@ -0,0 +1,113 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class AfflictionsFile : ContentFile + { + private readonly static ImmutableHashSet afflictionTypes; + static AfflictionsFile() + { + afflictionTypes = ReflectionUtils.GetDerivedNonAbstract() + .ToImmutableHashSet(); + } + + public AfflictionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + private void ParseElement(ContentXElement element, bool overriding) + { + Identifier elementName = element.NameAsIdentifier(); + if (element.IsOverride()) + { + element.Elements().ForEach(s => ParseElement(s, overriding: true)); + } + else if (elementName == "Afflictions") + { + element.Elements().ForEach(s => ParseElement(s, overriding: overriding)); + } + else if (elementName == "cprsettings") + { + var cprSettings = new CPRSettings(element, this); + CPRSettings.Prefabs.Add(cprSettings, overriding); + } + else if (elementName == "damageoverlay") + { +#if CLIENT + var damageOverlay = new CharacterHealth.DamageOverlayPrefab(element, this); + CharacterHealth.DamageOverlayPrefab.Prefabs.Add(damageOverlay, overriding); +#endif + } + else + { + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) + { + DebugConsole.ThrowError( + $"No identifier defined for the affliction '{elementName}' in file '{Path}'"); + return; + } + + if (AfflictionPrefab.Prefabs.ContainsKey(identifier)) + { + if (overriding) + { + DebugConsole.NewMessage( + $"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'", + Color.Yellow); + } + else + { + DebugConsole.ThrowError( + $"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'"); + return; + } + } + + var type = afflictionTypes.FirstOrDefault(t => + t.Name == elementName + || t.Name == $"Affliction{elementName}".ToIdentifier()) + ?? typeof(Affliction); + var prefab = CreatePrefab(element, type); + AfflictionPrefab.Prefabs.Add(prefab, overriding); + } + } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc?.Root is null) { return; } + ParseElement(doc.Root.FromPackage(ContentPackage), overriding: false); + } + + private AfflictionPrefab CreatePrefab(ContentXElement element, Type type) + { + if (type == typeof(AfflictionHusk)) { return new AfflictionPrefabHusk(element, this, type); } + return new AfflictionPrefab(element, this, type); + } + + public override void UnloadFile() + { +#if CLIENT + CharacterHealth.DamageOverlayPrefab.Prefabs.RemoveByFile(this); +#endif + CPRSettings.Prefabs.RemoveByFile(this); + AfflictionPrefab.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { +#if CLIENT + CharacterHealth.DamageOverlayPrefab.Prefabs.Sort(); +#endif + CPRSettings.Prefabs.Sort(); + AfflictionPrefab.Prefabs.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BackgroundCreaturePrefabsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BackgroundCreaturePrefabsFile.cs new file mode 100644 index 000000000..f9c370c8e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BackgroundCreaturePrefabsFile.cs @@ -0,0 +1,9 @@ +namespace Barotrauma +{ + sealed class BackgroundCreaturePrefabsFile : OtherFile + { + public BackgroundCreaturePrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + //this content type only comes into play when a level is generated, so LoadFile and UnloadFile don't have anything to do + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs new file mode 100644 index 000000000..1b45bc400 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs @@ -0,0 +1,19 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage, AlternativeContentTypeNames("MapCreature")] + sealed class BallastFloraFile : GenericPrefabFile + { + public BallastFloraFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "ballastflorabehavior"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "ballastflorabehaviors"; + protected override PrefabCollection prefabs => BallastFloraPrefab.Prefabs; + + protected override BallastFloraPrefab CreatePrefab(ContentXElement element) + { + return new BallastFloraPrefab(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BeaconStationFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BeaconStationFile.cs new file mode 100644 index 000000000..785fee918 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BeaconStationFile.cs @@ -0,0 +1,8 @@ +namespace Barotrauma +{ + [RequiredByCorePackage] + public class BeaconStationFile : BaseSubFile + { + public BeaconStationFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs new file mode 100644 index 000000000..717057ff6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class CaveGenerationParametersFile : GenericPrefabFile + { + public CaveGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "cave"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "cavegenerationparameters"; + protected override PrefabCollection prefabs => CaveGenerationParams.CaveParams; + protected override CaveGenerationParams CreatePrefab(ContentXElement element) + { + return new CaveGenerationParams(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs new file mode 100644 index 000000000..1a0b569d5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -0,0 +1,100 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class CharacterFile : ContentFile + { + public CharacterFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) + { + DebugConsole.ThrowError($"Loading character file failed: {Path}"); + return; + } + if (CharacterPrefab.Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.ContentFile == this))) + { + DebugConsole.ThrowError($"Duplicate path: {Path}"); + return; + } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool isOverride = mainElement.IsOverride(); + if (isOverride) { mainElement = mainElement.FirstElement(); } + if (!CharacterPrefab.CheckSpeciesName(mainElement, this, out Identifier n)) { return; } + var prefab = new CharacterPrefab(mainElement, this); + CharacterPrefab.Prefabs.Add(prefab, isOverride); + } + + public override void UnloadFile() + { + CharacterPrefab.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + CharacterPrefab.Prefabs.SortAll(); + } + + public override void Preload(Action addPreloadedSprite) + { +#if CLIENT + CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(Path.Value); + if (characterPrefab?.ConfigElement == null) + { + throw new Exception($"Failed to load the character config file from {Path}!"); + } + var mainElement = characterPrefab.ConfigElement; + mainElement.GetChildElements("sound").ForEach(e => RoundSound.Load(e)); + if (!CharacterPrefab.CheckSpeciesName(mainElement, this, out Identifier speciesName)) { return; } + bool humanoid = mainElement.GetAttributeBool("humanoid", false); + RagdollParams ragdollParams; + try + { + if (humanoid) + { + ragdollParams = RagdollParams.GetRagdollParams(speciesName); + } + else + { + ragdollParams = RagdollParams.GetRagdollParams(speciesName); + } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e); + return; + } + + if (ragdollParams != null) + { + HashSet texturePaths = new HashSet + { + ragdollParams.Texture + }; + foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs) + { + if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); } + if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); } + if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); } + foreach (var decorativeSprite in limb.decorativeSpriteParams) + { + if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); } + } + } + foreach (string texturePath in texturePaths) + { + addPreloadedSprite(new Sprite(texturePath, Vector2.Zero)); + } + } +#endif + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs new file mode 100644 index 000000000..9a6732858 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -0,0 +1,117 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + [AttributeUsage(AttributeTargets.Class, Inherited = true)] + public class NotSyncedInMultiplayer : Attribute { } + + /// + /// Base class for content file types, which are loaded + /// from filelist.xml via reflection. + /// PLEASE AVOID INHERITING FROM THIS CLASS DIRECTLY. + /// Inheriting from GenericPrefabFile<T> is likely what + /// you want. + /// + public abstract class ContentFile + { + public class TypeInfo + { + public readonly Type Type; + public readonly bool RequiredByCorePackage; + public readonly bool NotSyncedInMultiplayer; + public readonly ImmutableHashSet? AlternativeTypes; + public readonly ImmutableHashSet Names; + + public TypeInfo(Type type) + { + Type = type; + + var reqByCoreAttribute = type.GetCustomAttribute(); + RequiredByCorePackage = reqByCoreAttribute != null; + var notSyncedInMultiplayerAttribute = type.GetCustomAttribute(); + NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null; + AlternativeTypes = reqByCoreAttribute?.AlternativeTypes; + + HashSet names = new HashSet { type.Name.RemoveFromEnd("File").ToIdentifier() }; + if (type.GetCustomAttribute()?.Names is { } altNames) + { + names.UnionWith(altNames); + } + + Names = names.ToImmutableHashSet(); + } + + public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) => + (ContentFile?)Activator.CreateInstance(Type, contentPackage, path); + } + + public readonly static ImmutableHashSet Types; + static ContentFile() + { + Types = ReflectionUtils.GetDerivedNonAbstract() + .Select(t => new TypeInfo(t)) + .ToImmutableHashSet(); + } + + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) + { + Result fail(string error) + => Result.Failure(error); + + Identifier elemName = element.NameAsIdentifier(); + var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); + var filePath = element.GetAttributeContentPath("file", contentPackage); + if (type is null) + { + return fail($"Invalid content type \"{elemName}\""); + } + + if (filePath is null) + { + return fail($"No content path defined for file of type \"{elemName}\""); + } + try + { + var file = type.CreateInstance(contentPackage, filePath); + return file is null + ? throw new Exception($"Content type is not implemented correctly") + : Result.Success(file); + } + catch (Exception e) + { + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}\n{e.StackTrace.CleanupStackTrace()}"); + } + } + + protected ContentFile(ContentPackage contentPackage, ContentPath path) + { + ContentPackage = contentPackage; + Path = path; + Hash = CalculateHash(); + } + + public readonly ContentPackage ContentPackage; + public readonly ContentPath Path; + public readonly Md5Hash Hash; + public abstract void LoadFile(); + public abstract void UnloadFile(); + public abstract void Sort(); + + public virtual void Preload(Action addPreloadedSprite) { } + + public virtual Md5Hash CalculateHash() + { + return Md5Hash.CalculateForFile(Path.Value, Md5Hash.StringHashOptions.IgnoreWhitespace); + } + + public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs new file mode 100644 index 000000000..b9eb4ddce --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class CorpsesFile : GenericPrefabFile + { + public CorpsesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "corpse"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "corpses"; + protected override PrefabCollection prefabs => CorpsePrefab.Prefabs; + protected override CorpsePrefab CreatePrefab(ContentXElement element) + { + return new CorpsePrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DecalsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DecalsFile.cs new file mode 100644 index 000000000..9c2562f04 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DecalsFile.cs @@ -0,0 +1,22 @@ +namespace Barotrauma +{ + public sealed class DecalsFile : ContentFile + { + public DecalsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + DecalManager.LoadFromFile(this); + } + + public override void UnloadFile() + { + DecalManager.RemoveByFile(this); + } + + public override void Sort() + { + DecalManager.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EnemySubmarineFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EnemySubmarineFile.cs new file mode 100644 index 000000000..61f3b8651 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EnemySubmarineFile.cs @@ -0,0 +1,8 @@ +namespace Barotrauma +{ + [RequiredByCorePackage] + public class EnemySubmarineFile : BaseSubFile + { + public EnemySubmarineFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs new file mode 100644 index 000000000..298f618c5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs @@ -0,0 +1,17 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class EventManagerSettingsFile : GenericPrefabFile + { + public EventManagerSettingsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "EventManagerSettings"; + protected override PrefabCollection prefabs => EventManagerSettings.Prefabs; + protected override EventManagerSettings CreatePrefab(ContentXElement element) + { + return new EventManagerSettings(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs new file mode 100644 index 000000000..bb200e5c6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class FactionsFile : GenericPrefabFile + { + public FactionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "faction"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "factions"; + protected override PrefabCollection prefabs => FactionPrefab.Prefabs; + protected override FactionPrefab CreatePrefab(ContentXElement element) + { + return new FactionPrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs new file mode 100644 index 000000000..794968346 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + abstract class GenericPrefabFile : ContentFile where T : Prefab + { + protected GenericPrefabFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected abstract bool MatchesSingular(Identifier identifier); + protected abstract bool MatchesPlural(Identifier identifier); + protected abstract PrefabCollection prefabs { get; } + protected abstract T CreatePrefab(ContentXElement element); + + private void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = parentElement.NameAsIdentifier(); + var childElements = parentElement.Elements() +#if DEBUG + .OrderBy(e => Rand.Int(int.MaxValue, Rand.RandSync.Unsynced)).ToArray() +#endif + ; + if (parentElement.IsOverride()) + { + foreach (var element in childElements) + { + LoadFromXElement(element, true); + } + } + else if (elemName == "clear") + { + prefabs.AddOverrideFile(this); + } + else if (MatchesSingular(elemName)) + { + T prefab = CreatePrefab(parentElement); + prefabs.Add(prefab, overriding); + } + else if (MatchesPlural(elemName)) + { + foreach (var element in childElements) + { + LoadFromXElement(element, overriding); + } + } + else + { + DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + } + } + + public override sealed void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override sealed void UnloadFile() + { + prefabs.RemoveByFile(this); + } + + public sealed override void Sort() + { + prefabs.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/HashlessFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/HashlessFile.cs new file mode 100644 index 000000000..a76a64ea0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/HashlessFile.cs @@ -0,0 +1,10 @@ +namespace Barotrauma +{ + [NotSyncedInMultiplayer] + public abstract class HashlessFile : ContentFile + { + public HashlessFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public sealed override Md5Hash CalculateHash() => Md5Hash.Blank; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs new file mode 100644 index 000000000..71f5cd664 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs @@ -0,0 +1,17 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class ItemAssemblyFile : GenericPrefabFile + { + public ItemAssemblyFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "itemassembly"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "itemassemblies"; + protected override PrefabCollection prefabs => ItemAssemblyPrefab.Prefabs; + protected override ItemAssemblyPrefab CreatePrefab(ContentXElement element) + { + return new ItemAssemblyPrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs new file mode 100644 index 000000000..5065470c2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class ItemFile : GenericPrefabFile + { + public ItemFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "items"; + protected override PrefabCollection prefabs => ItemPrefab.Prefabs; + protected override ItemPrefab CreatePrefab(ContentXElement element) + { + return new ItemPrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs new file mode 100644 index 000000000..7c2291e84 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs @@ -0,0 +1,60 @@ +using Barotrauma; +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + + [RequiredByCorePackage] + sealed class JobsFile : ContentFile + { + public JobsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + LoadElements(doc.Root.FromPackage(ContentPackage), false); + } + + private void LoadElements(ContentXElement mainElement, bool isOverride) + { + foreach (var element in mainElement.Elements()) + { + if (element.NameAsIdentifier() == "nojob") + { + JobPrefab.NoJobElement ??= element; + } + else if (element.NameAsIdentifier() == "ItemRepairPriorities") + { + foreach (var subElement in element.Elements()) + { + ItemRepairPriority prio = new ItemRepairPriority(subElement, this); + ItemRepairPriority.Prefabs.Add(prio, isOverride); + } + } + else if (element.IsOverride()) + { + LoadElements(element, true); + } + else + { + var job = new JobPrefab(element, this); + JobPrefab.Prefabs.Add(job, isOverride); + } + } + } + + public override void UnloadFile() + { + JobPrefab.Prefabs.RemoveByFile(this); + ItemRepairPriority.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + JobPrefab.Prefabs.SortAll(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelGenerationParametersFile.cs new file mode 100644 index 000000000..b590cc063 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelGenerationParametersFile.cs @@ -0,0 +1,68 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class LevelGenerationParametersFile : ContentFile + { + public LevelGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + private void LoadBiomes(ContentXElement element, bool isOverride) + { + foreach (var subElement in element.Elements()) + { + Biome biome = new Biome(subElement, this); + Biome.Prefabs.Add(biome, isOverride); + } + } + + private void LoadLevelGenerationParams(ContentXElement element, bool isOverride) + { + LevelGenerationParams lParams = new LevelGenerationParams(element, this); + LevelGenerationParams.LevelParams.Add(lParams, isOverride); + } + + private void LoadSubElements(ContentXElement element, bool overridePropagation) + { + foreach (var subElement in element.Elements()) + { + if (subElement.IsOverride()) + { + LoadSubElements(subElement, true); + } + else if (subElement.NameAsIdentifier() == "clear") + { + LevelGenerationParams.LevelParams.AddOverrideFile(this); + Biome.Prefabs.AddOverrideFile(this); + } + else if (subElement.NameAsIdentifier() == "biomes") + { + LoadBiomes(subElement, overridePropagation); + } + else + { + LoadLevelGenerationParams(subElement, overridePropagation); + } + } + } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc is null) { return; } + LoadSubElements(doc.Root.FromPackage(ContentPackage), false); + } + + public override void UnloadFile() + { + LevelGenerationParams.LevelParams.RemoveByFile(this); + Biome.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + LevelGenerationParams.LevelParams.SortAll(); + Biome.Prefabs.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs new file mode 100644 index 000000000..228dca7cf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class LevelObjectPrefabsFile : GenericPrefabFile + { + public LevelObjectPrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "levelobjects"; + protected override PrefabCollection prefabs => LevelObjectPrefab.Prefabs; + protected override LevelObjectPrefab CreatePrefab(ContentXElement element) + { + return new LevelObjectPrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs new file mode 100644 index 000000000..cd3cc4c91 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class LocationTypesFile : GenericPrefabFile + { + public LocationTypesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "locationtypes"; + protected override PrefabCollection prefabs => LocationType.Prefabs; + protected override LocationType CreatePrefab(ContentXElement element) + { + return new LocationType(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MapGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MapGenerationParametersFile.cs new file mode 100644 index 000000000..b45d9bbf5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MapGenerationParametersFile.cs @@ -0,0 +1,34 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class MapGenerationParametersFile : ContentFile + { + public MapGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) + { + DebugConsole.ThrowError($"Loading map generation parameters file failed: {Path}"); + return; + } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool isOverride = mainElement.IsOverride(); + if (isOverride) { mainElement = mainElement.FirstElement(); } + var prefab = new MapGenerationParams(mainElement, this); + MapGenerationParams.Params.Add(prefab, isOverride); + } + + public override void UnloadFile() + { + MapGenerationParams.Params.RemoveByFile(this); + } + + public override void Sort() + { + MapGenerationParams.Params.Sort(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs new file mode 100644 index 000000000..11efb2d0c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class MissionsFile : GenericPrefabFile + { + /*private readonly static ImmutableHashSet missionTypes; + static MissionsFile() + { + missionTypes = ReflectionUtils.GetDerivedNonAbstract() + .ToImmutableHashSet(); + }*/ + + public MissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) + => !MatchesPlural(identifier); + /*missionTypes.Any(t => identifier == t.Name) + || identifier == "OutpostDestroyMission" || identifier == "OutpostRescueMission";*/ + protected override bool MatchesPlural(Identifier identifier) => identifier == "missions"; + protected override PrefabCollection prefabs => MissionPrefab.Prefabs; + protected override MissionPrefab CreatePrefab(ContentXElement element) + { + return new MissionPrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs new file mode 100644 index 000000000..f26a17cd7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class NPCConversationsFile : ContentFile + { + public NPCConversationsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool allowOverriding = doc.Root.IsOverride(); + if (allowOverriding) + { + mainElement = mainElement.FirstElement(); + } + + var npcConversationCollection = new NPCConversationCollection(this, mainElement); + if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language)) + { + NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection()); + } + NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding); + } + + public override void UnloadFile() + { + foreach (var collection in NPCConversationCollection.Collections.Values) + { + collection.RemoveByFile(this); + } + } + + public override void Sort() + { + foreach (var collection in NPCConversationCollection.Collections.Values) + { + collection.SortAll(); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs new file mode 100644 index 000000000..85b2548d9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class NPCSetsFile : GenericPrefabFile + { + public NPCSetsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "npcset"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "npcsets"; + protected override PrefabCollection prefabs => NPCSet.Sets; + protected override NPCSet CreatePrefab(ContentXElement element) + { + return new NPCSet(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs new file mode 100644 index 000000000..5699a0410 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs @@ -0,0 +1,70 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + #warning TODO: this is almost a GenericPrefabFile. Must refactor further. + [RequiredByCorePackage] + sealed class OrdersFile : ContentFile + { + public OrdersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = new Identifier(parentElement.Name.ToString()); + if (parentElement.IsOverride()) + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, true); + } + } + else if (elemName == "order") + { + OrderPrefab prefab = new OrderPrefab(parentElement, this); + OrderPrefab.Prefabs.Add(prefab, overriding); + } + else if (elemName == "ordercategory") + { + OrderCategoryIcon prefab = new OrderCategoryIcon(parentElement, this); + OrderCategoryIcon.OrderCategoryIcons.Add(prefab, overriding); + } + else if (elemName == "orders") + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, overriding); + } + } + else if (elemName == "clear") + { + OrderCategoryIcon.OrderCategoryIcons.AddOverrideFile(this); + OrderPrefab.Prefabs.AddOverrideFile(this); + } + else + { + DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + } + } + + public override sealed void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override sealed void UnloadFile() + { + OrderCategoryIcon.OrderCategoryIcons.RemoveByFile(this); + OrderPrefab.Prefabs.RemoveByFile(this); + } + + public override sealed void Sort() + { + OrderCategoryIcon.OrderCategoryIcons.SortAll(); + OrderPrefab.Prefabs.SortAll(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs new file mode 100644 index 000000000..57b5eaf64 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs @@ -0,0 +1,16 @@ +using Barotrauma; + +namespace Barotrauma +{ + + [AlternativeContentTypeNames("None")] + public class OtherFile : HashlessFile + { + public OtherFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + //this content type is completely ignored by the game so LoadFile and UnloadFile don't do anything + public sealed override void LoadFile() { } + public sealed override void UnloadFile() { } + public sealed override void Sort() { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs new file mode 100644 index 000000000..1972243dc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage(alternativeTypes: typeof(OutpostFile))] + sealed class OutpostConfigFile : GenericPrefabFile + { + public OutpostConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "OutpostConfig"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "OutpostGenerationParameters"; + protected override PrefabCollection prefabs => OutpostGenerationParams.OutpostParams; + protected override OutpostGenerationParams CreatePrefab(ContentXElement element) + { + return new OutpostGenerationParams(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostFile.cs new file mode 100644 index 000000000..e03d31d3b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostFile.cs @@ -0,0 +1,7 @@ +namespace Barotrauma +{ + public class OutpostFile : BaseSubFile + { + public OutpostFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostModuleFIle.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostModuleFIle.cs new file mode 100644 index 000000000..eb673d840 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostModuleFIle.cs @@ -0,0 +1,7 @@ +namespace Barotrauma +{ + public class OutpostModuleFile : BaseSubFile + { + public OutpostModuleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs new file mode 100644 index 000000000..de128d17c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs @@ -0,0 +1,31 @@ +using System.Xml.Linq; +#if CLIENT +using Barotrauma.Particles; +#endif + +namespace Barotrauma +{ + [RequiredByCorePackage] + [NotSyncedInMultiplayer] +#if CLIENT + sealed class ParticlesFile : GenericPrefabFile + { + public ParticlesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "particles"; + protected override PrefabCollection prefabs => ParticlePrefab.Prefabs; + protected override ParticlePrefab CreatePrefab(ContentXElement element) + { + return new ParticlePrefab(element, this); + } + + public override Md5Hash CalculateHash() => Md5Hash.Blank; + } +#else + sealed class ParticlesFile : OtherFile + { + public ParticlesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } //this content type doesn't do anything on a server + } +#endif +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs new file mode 100644 index 000000000..d4c5b1c43 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -0,0 +1,91 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class RandomEventsFile : ContentFile + { + public RandomEventsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = new Identifier(parentElement.Name.ToString()); + if (parentElement.IsOverride()) + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, true); + } + } + else if (elemName == "randomevents") + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, overriding); + } + } + else if (elemName == "eventprefabs") + { + foreach (var subElement in parentElement.Elements()) + { + var prefab = new EventPrefab(subElement, this); + EventPrefab.Prefabs.Add(prefab, overriding); + } + } + else if (elemName == "eventsprites") + { +#if CLIENT + foreach (var subElement in parentElement.Elements()) + { + var prefab = new EventSprite(subElement, this); + EventSprite.Prefabs.Add(prefab, overriding); + } +#endif + } + else if (elemName == "eventset") + { + var prefab = new EventSet(parentElement, this); + EventSet.Prefabs.Add(prefab, overriding); + } + else if (elemName == "clear") + { + EventPrefab.Prefabs.AddOverrideFile(this); + EventSet.Prefabs.AddOverrideFile(this); +#if CLIENT + EventSprite.Prefabs.AddOverrideFile(this); +#endif + } + else + { + DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + } + } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override void UnloadFile() + { + EventPrefab.Prefabs.RemoveByFile(this); + EventSet.Prefabs.RemoveByFile(this); +#if CLIENT + EventSprite.Prefabs.RemoveByFile(this); +#endif + } + + public override void Sort() + { + EventPrefab.Prefabs.SortAll(); + EventSet.Prefabs.SortAll(); +#if CLIENT + EventSprite.Prefabs.SortAll(); +#endif + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs new file mode 100644 index 000000000..9a8de8fd1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs @@ -0,0 +1,19 @@ +using Barotrauma.RuinGeneration; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class RuinConfigFile : GenericPrefabFile + { + public RuinConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "RuinConfig"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "RuinGenerationParameters"; + protected override PrefabCollection prefabs => RuinGenerationParams.RuinParams; + protected override RuinGenerationParams CreatePrefab(ContentXElement element) + { + return new RuinGenerationParams(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SkillSettingsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SkillSettingsFile.cs new file mode 100644 index 000000000..01216f3bf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SkillSettingsFile.cs @@ -0,0 +1,33 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class SkillSettingsFile : ContentFile + { + public SkillSettingsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool allowOverriding = mainElement.IsOverride(); + if (allowOverriding) + { + mainElement = mainElement.FirstElement(); + } + var prefab = new SkillSettings(mainElement, this); + SkillSettings.Prefabs.Add(prefab, allowOverriding); + } + + public override void UnloadFile() + { + SkillSettings.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + SkillSettings.Prefabs.Sort(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs new file mode 100644 index 000000000..57034f4d1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Immutable; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] +#if CLIENT + sealed class SoundsFile : GenericPrefabFile + { + public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override PrefabCollection prefabs => SoundPrefab.Prefabs; + + protected override SoundPrefab CreatePrefab(ContentXElement element) + { + var elemName = element.NameAsIdentifier(); + if (SoundPrefab.TagToDerivedPrefab.ContainsKey(elemName)) + { + return Activator.CreateInstance(SoundPrefab.TagToDerivedPrefab[elemName], new object[] { element, this }) as SoundPrefab; + } + return new SoundPrefab(element, this); + } + + protected override bool MatchesPlural(Identifier identifier) => identifier == "sounds"; + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + + public override Md5Hash CalculateHash() => Md5Hash.Blank; + } +#else + sealed class SoundsFile : OtherFile + { + public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +#endif +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs new file mode 100644 index 000000000..b961311ab --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class StructureFile : GenericPrefabFile + { + public StructureFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "structures"; + protected override PrefabCollection prefabs => StructurePrefab.Prefabs; + protected override StructurePrefab CreatePrefab(ContentXElement element) + { + return new StructurePrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs new file mode 100644 index 000000000..dcd3107ff --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs @@ -0,0 +1,38 @@ +using System; +using System.Security.Cryptography; + +namespace Barotrauma +{ + public abstract class BaseSubFile : ContentFile + { + protected BaseSubFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) + { + using var md5 = MD5.Create(); + #warning TODO: this doesn't account for collisions, this should probably be using the PrefabCollection class like everything else + UintIdentifier = ToolBox.StringToUInt32Hash(Barotrauma.IO.Path.GetFileNameWithoutExtension(path.Value), md5); + } + + public readonly UInt32 UintIdentifier; + + public override void LoadFile() + { + SubmarineInfo.RefreshSavedSub(Path.Value); + } + + public override void UnloadFile() + { + SubmarineInfo.RefreshSavedSub(Path.Value); + } + + public override void Sort() + { + //Overrides for subs don't exist! Should we change this? + } + } + + [NotSyncedInMultiplayer] + public class SubmarineFile : BaseSubFile + { + public SubmarineFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs new file mode 100644 index 000000000..6ca1f9c68 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class TalentTreesFile : GenericPrefabFile + { + public TalentTreesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "talenttree"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "talenttrees"; + protected override PrefabCollection prefabs => TalentTree.JobTalentTrees; + protected override TalentTree CreatePrefab(ContentXElement element) + { + return new TalentTree(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs new file mode 100644 index 000000000..1b5b05f4f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class TalentsFile : GenericPrefabFile + { + public TalentsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "talent"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "talents"; + protected override PrefabCollection prefabs => TalentPrefab.TalentPrefabs; + protected override TalentPrefab CreatePrefab(ContentXElement element) + { + return new TalentPrefab(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs new file mode 100644 index 000000000..de37b623a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + public sealed class TextFile : ContentFile + { + public TextFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + var mainElement = doc.Root.FromPackage(ContentPackage); + + var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value); + + LanguageIdentifier language = languageName.ToLanguageIdentifier(); + if (!TextManager.TextPacks.ContainsKey(language)) + { + TextManager.TextPacks.TryAdd(language, ImmutableHashSet.Empty); + } + + var newPack = new TextPack(this, mainElement, language); + var newHashSet = TextManager.TextPacks[language].Add(newPack); + TextManager.TextPacks.TryRemove(language, out _); + TextManager.TextPacks.TryAdd(language, newHashSet); + TextManager.IncrementLanguageVersion(); + } + + public override void UnloadFile() + { + foreach (var kvp in TextManager.TextPacks.ToArray()) + { + var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet(); + TextManager.TextPacks.TryRemove(kvp.Key, out _); + if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); } + } + TextManager.IncrementLanguageVersion(); + } + + public override void Sort() + { + //Overrides for text packs don't exist! Should we change this? + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs new file mode 100644 index 000000000..3a58364e0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs @@ -0,0 +1,26 @@ +using System.Xml.Linq; + +#warning TODO: This file is just about the only thing that's actually somewhat okay about the current traitor system. Gut the whole thing. + +#if CLIENT +using PrefabType = Barotrauma.TraitorMissionPrefab; +#elif SERVER +using PrefabType = Barotrauma.TraitorMissionPrefab.TraitorMissionEntry; +#endif + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class TraitorMissionsFile : GenericPrefabFile + { + public TraitorMissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions"; + protected override PrefabCollection prefabs => PrefabType.Prefabs; + protected override PrefabType CreatePrefab(ContentXElement element) + { + return new PrefabType(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UIStyleFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UIStyleFile.cs new file mode 100644 index 000000000..78571aec0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UIStyleFile.cs @@ -0,0 +1,100 @@ +using System; +using Barotrauma.Extensions; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ +#if CLIENT + public sealed class UIStyleFile : HashlessFile + { + public UIStyleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = parentElement.NameAsIdentifier(); + Identifier elemNameWithFontSuffix = elemName.AppendIfMissing("Font"); + if (parentElement.IsOverride()) + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, true); + } + } + else if (GUIStyle.Fonts.ContainsKey(elemNameWithFontSuffix)) + { + GUIFontPrefab prefab = new GUIFontPrefab(parentElement, this); + GUIStyle.Fonts[elemNameWithFontSuffix].Prefabs.Add(prefab, overriding); + } + else if (GUIStyle.Sprites.ContainsKey(elemName)) + { + GUISpritePrefab prefab = new GUISpritePrefab(parentElement, this); + GUIStyle.Sprites[elemName].Prefabs.Add(prefab, overriding); + } + else if (GUIStyle.SpriteSheets.ContainsKey(elemName)) + { + GUISpriteSheetPrefab prefab = new GUISpriteSheetPrefab(parentElement, this); + GUIStyle.SpriteSheets[elemName].Prefabs.Add(prefab, overriding); + } + else if (GUIStyle.Colors.ContainsKey(elemName)) + { + GUIColorPrefab prefab = new GUIColorPrefab(parentElement, this); + GUIStyle.Colors[elemName].Prefabs.Add(prefab, overriding); + } + else if (elemName == "cursor") + { + GUICursorPrefab prefab = new GUICursorPrefab(parentElement, this); + GUIStyle.CursorSprite.Prefabs.Add(prefab, overriding); + } + else if (elemName == "style") + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, overriding); + } + } + else + { + GUIComponentStyle prefab = new GUIComponentStyle(parentElement, this); + GUIStyle.ComponentStyles.Add(prefab, overriding); + } + } + + public override sealed void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override sealed void UnloadFile() + { + GUIStyle.ComponentStyles.RemoveByFile(this); + GUIStyle.CursorSprite.Prefabs.RemoveByFile(this); + GUIStyle.Fonts.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + GUIStyle.Sprites.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + GUIStyle.SpriteSheets.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + GUIStyle.Colors.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + } + + public override sealed void Sort() + { + GUIStyle.ComponentStyles.SortAll(); + GUIStyle.CursorSprite.Prefabs.Sort(); + GUIStyle.Fonts.Values.ForEach(p => p.Prefabs.Sort()); + GUIStyle.Sprites.Values.ForEach(p => p.Prefabs.Sort()); + GUIStyle.SpriteSheets.Values.ForEach(p => p.Prefabs.Sort()); + GUIStyle.Colors.Values.ForEach(p => p.Prefabs.Sort()); + } + } +#else + public sealed class UIStyleFile : OtherFile + { + public UIStyleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +#endif +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs new file mode 100644 index 000000000..61de9e96b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs @@ -0,0 +1,31 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class UpgradeModulesFile : GenericPrefabFile + { + public UpgradeModulesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => + identifier == "upgrademodule" || + identifier == "upgradecategory"; + + protected override bool MatchesPlural(Identifier identifier) => + identifier == "upgrademodules"; + + protected override PrefabCollection prefabs => UpgradeContentPrefab.PrefabsAndCategories; + protected override UpgradeContentPrefab CreatePrefab(ContentXElement element) + { + Identifier elemName = element.NameAsIdentifier(); + if (elemName == "upgradecategory") + { + return new UpgradeCategory(element, this); + } + else + { + return new UpgradePrefab(element, this); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs new file mode 100644 index 000000000..54a445ff0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class WreckAIConfigFile : GenericPrefabFile + { + public WreckAIConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "wreckaiconfig"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "wreckaiconfigs"; + protected override PrefabCollection prefabs => WreckAIConfig.Prefabs; + protected override WreckAIConfig CreatePrefab(ContentXElement element) + { + return new WreckAIConfig(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckFile.cs new file mode 100644 index 000000000..89dfa873c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckFile.cs @@ -0,0 +1,7 @@ +namespace Barotrauma +{ + public class WreckFile : BaseSubFile + { + public WreckFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs new file mode 100644 index 000000000..c097e2aa4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -0,0 +1,306 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Steam; + +namespace Barotrauma +{ + public abstract class ContentPackage + { + #warning TODO: make this independent of the current version + public static readonly Version MinimumHashCompatibleVersion = GameMain.Version; + + public const string LocalModsDir = "LocalMods"; + public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( + SaveUtil.SaveFolder, + "WorkshopMods", + "Installed"); + + public const string FileListFileName = "filelist.xml"; + public const string DefaultModVersion = "1.0.0"; + + public readonly string Name; + public readonly ImmutableArray AltNames; + public readonly string Path; + public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? ""; + public readonly UInt64 SteamWorkshopId; + + public readonly Version GameVersion; + public readonly string ModVersion; + public readonly Md5Hash Hash; + public readonly DateTime? InstallTime; + + public readonly ImmutableArray Files; + public readonly ImmutableArray Errors; + + public async Task IsUpToDate() + { + if (SteamWorkshopId != 0 && InstallTime.HasValue) + { + Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId); + if (item is null) { return true; } + return item.Value.LatestUpdateTime <= InstallTime; + } + return true; + } + + public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); + + #warning TODO: remove this, unless we truly believe that determining "multiplayer-incompatible content" is something we should do + public readonly bool HasMultiplayerIncompatibleContent; + + protected ContentPackage(XDocument doc, string path) + { + Path = path.CleanUpPathCrossPlatform(); + XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); + + Name = rootElement.GetAttributeString("name", "").Trim(); + AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty()) + .Select(n => n.Trim()).ToImmutableArray(); + AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty"); + SteamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); + + GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); + ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); + if (rootElement.Attribute("installtime") != null) + { + InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0)); + } + else + { + InstallTime = null; + } + + var fileResults = rootElement.Elements() + .Select(e => ContentFile.CreateFromXElement(this, e)) + .ToArray(); + + Files = fileResults + .OfType>() + .Select(f => f.Value) + .ToImmutableArray(); + + Errors = fileResults + .OfType>() + .Select(f => f.Error) + .ToImmutableArray(); + + HasMultiplayerIncompatibleContent = Files.Any(f => !f.NotSyncedInMultiplayer); + + Hash = CalculateHash(); + var expectedHash = rootElement.GetAttributeString("expectedhash", ""); + if (HashMismatches(expectedHash)) + { + DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})"); + } + } + + public bool HashMismatches(string expectedHash) + => GameVersion >= MinimumHashCompatibleVersion && + !expectedHash.IsNullOrWhiteSpace() && + !expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase); + + public IEnumerable GetFiles() where T : ContentFile => Files.Where(f => f is T).Cast(); + + public IEnumerable GetFiles(Type type) + => !type.IsSubclassOf(typeof(ContentFile)) + ? throw new ArgumentException($"Type must be subclass of ContentFile, got {type.Name}") + : Files.Where(f => f.GetType() == type || f.GetType().IsSubclassOf(type)); + + public bool NameMatches(Identifier name) + => Name == name || AltNames.Any(n => n == name); + + public bool NameMatches(string name) + => NameMatches(name.ToIdentifier()); + + public static ContentPackage? TryLoad(string path) + { + XDocument doc = XMLExtensions.TryLoadXml(path); + + try + { + if (doc.Root.GetAttributeBool("corepackage", false)) + { + return new CorePackage(doc, path); + } + else + { + return new RegularPackage(doc, path); + } + } + catch (Exception e) + { + while (e.InnerException != null) { e = e.InnerException; } + DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}"); + return null; + } + } + + public Md5Hash CalculateHash(bool logging = false) + { + using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5); + + if (logging) + { + DebugConsole.NewMessage("****************************** Calculating content package hash " + Name); + } + + foreach (ContentFile file in Files) + { + try + { + var hash = file.Hash; + if (logging) + { + DebugConsole.NewMessage(" " + file.Path + ": " + hash.StringRepresentation); + } + incrementalHash.AppendData(hash.ByteRepresentation); + } + + catch (Exception e) + { + DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e); + break; + } + } + + var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset()); + if (logging) + { + DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.StringRepresentation); + } + + return md5Hash; + } + + protected void AssertCondition(bool condition, string errorMsg) + { + if (!condition) + { + throw new InvalidOperationException($"Failed to load \"{Name ?? Path}\": {errorMsg}"); + } + } + + public void LoadFilesOfType() where T : ContentFile + { + Files.Where(f => f is T).ForEach(f => f.LoadFile()); + } + + public void UnloadFilesOfType() where T : ContentFile + { + Files.Where(f => f is T).ForEach(f => f.UnloadFile()); + } + + public enum LoadResult + { + Success, + Failure + } + + public LoadResult LoadPackage() + { + foreach (var p in LoadPackageEnumerable()) + { + if (p.Exception != null) { return LoadResult.Failure; } + } + return LoadResult.Success; + } + + public IEnumerable LoadPackageEnumerable() + { + ContentFile[] getFilesToLoad(Predicate predicate) + => Files.Where(predicate.Invoke).ToArray() +#if DEBUG + //The game should be able to work just fine with a completely arbitrary file load order. + //To make sure we don't mess this up, debug builds randomize it so it has a higher chance + //of breaking anything that's not implemented correctly. + .Randomize() +#endif + ; + + IEnumerable loadFiles(ContentFile[] filesToLoad, int indexOffset) + { + for (int i = 0; i < filesToLoad.Length; i++) + { + Exception? exception = null; + try + { + //do not allow exceptions thrown here to crash the game + filesToLoad[i].LoadFile(); + } + catch (Exception e) + { + exception = e; + } + if (exception != null) + { + yield return ContentPackageManager.LoadProgress.Failure(exception); + break; + } + yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length); + } + } + + //Load the UI files first. This is to allow the game to render + //the text in the loading screen as soon as possible. + var priorityFiles = getFilesToLoad(f => f is UIStyleFile); + + var remainder = getFilesToLoad(f => !priorityFiles.Contains(f)); + + var loadEnumerable = + loadFiles(priorityFiles, 0) + .Concat(loadFiles(remainder, priorityFiles.Length)); + + foreach (var p in loadEnumerable) + { + if (p.Exception != null) + { + HandleLoadException(p.Exception); + yield return p; + break; + } + yield return p; + } + } + + protected abstract void HandleLoadException(Exception e); + + public void UnloadPackage() + { + Files.ForEach(f => f.UnloadFile()); + } + + public override int GetHashCode() + { + byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4)); + return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3]; + } + + public static bool PathAllowedAsLocalModFile(string path) + { +#if DEBUG + if (GameMain.VanillaContent.Files.Any(f => f.Path == path)) + { + //file is in vanilla package, this is allowed + return true; + } +#endif + + while (true) + { + string temp = Barotrauma.IO.Path.GetDirectoryName(path) ?? ""; + if (string.IsNullOrEmpty(temp)) { break; } + path = temp; + } + return path == LocalModsDir; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs new file mode 100644 index 000000000..33daaec39 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -0,0 +1,52 @@ +using Barotrauma.Extensions; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ + [AttributeUsage(AttributeTargets.Class)] + public class RequiredByCorePackage : Attribute + { + public readonly ImmutableHashSet AlternativeTypes; + public RequiredByCorePackage(params Type[] alternativeTypes) + { + AlternativeTypes = alternativeTypes.ToImmutableHashSet(); + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class AlternativeContentTypeNames : Attribute + { + public readonly ImmutableHashSet Names; + public AlternativeContentTypeNames(params string[] names) + { + Names = names.ToIdentifiers().ToImmutableHashSet(); + } + } + + public class CorePackage : ContentPackage + { + public CorePackage(XDocument doc, string path) : base(doc, path) + { + AssertCondition(doc.Root.GetAttributeBool("corepackage", false), + "Expected a core package, got a regular package"); + + var missingFileTypes = ContentFile.Types.Where( + t => t.RequiredByCorePackage + && !Files.Any(f => t.Type == f.GetType() + || t.AlternativeTypes.Contains(f.GetType()))); + AssertCondition(!missingFileTypes.Any(), + "Core package requires at least one of the following content types: " + + string.Join(", ", missingFileTypes.Select(t => t.Type.Name))); + } + + protected override void HandleLoadException(Exception e) + { + throw new Exception($"An exception was thrown while loading \"{Name}\"", e); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs new file mode 100644 index 000000000..b66bb5c10 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs @@ -0,0 +1,19 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + public class RegularPackage : ContentPackage + { + public RegularPackage(XDocument doc, string path) : base(doc, path) + { + AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package"); + } + + protected override void HandleLoadException(Exception e) + { + UnloadPackage(); + DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs new file mode 100644 index 000000000..58d0bd8a4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -0,0 +1,461 @@ +#nullable enable +using Barotrauma.Extensions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Steam; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public static partial class ContentPackageManager + { + public const string CopyIndicatorFileName = ".copying"; + public const string VanillaFileList = "Content/ContentPackages/Vanilla.xml"; + + public const string CorePackageElementName = "corepackage"; + public const string RegularPackagesElementName = "regularpackages"; + public const string RegularPackagesSubElementName = "package"; + + public static class EnabledPackages + { + public static CorePackage? Core { get; private set; } = null; + + private static readonly List regular = new List(); + public static IReadOnlyList Regular => regular; + + public static IEnumerable All => + Core != null + ? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular) + : Enumerable.Empty(); + + private static class BackupPackages + { + public static CorePackage? Core; + public static ImmutableArray? Regular; + } + + public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume(); + + public static IEnumerable SetCoreEnumerable(CorePackage newCore) + { + var oldCore = Core; + if (newCore == oldCore) { yield break; } + Core?.UnloadPackage(); + Core = newCore; + foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; } + ThrowIfDuplicates(All); + yield return new LoadProgress(1.0f); + } + + public static void ReloadCore() + { + if (Core == null) { return; } + Core.UnloadPackage(); + Core.LoadPackage(); + ThrowIfDuplicates(All); + } + + public static void EnableRegular(RegularPackage p) + { + if (regular.Contains(p)) { return; } + + var newRegular = regular.ToList(); + newRegular.Add(p); + SetRegular(newRegular); + } + + public static void SetRegular(IReadOnlyList newRegular) + => SetRegularEnumerable(newRegular).Consume(); + + public static IEnumerable SetRegularEnumerable(IReadOnlyList inNewRegular) + { + if (ReferenceEquals(inNewRegular, regular)) { yield break; } + if (inNewRegular.SequenceEqual(regular)) { yield break; } + ThrowIfDuplicates(inNewRegular); + var newRegular = inNewRegular.ToList(); + IEnumerable toUnload = regular.Where(r => !newRegular.Contains(r)); + RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray(); + toUnload.ForEach(r => r.UnloadPackage()); + + Range loadingRange = new Range(0.0f, 1.0f); + + for (int i = 0; i < toLoad.Length; i++) + { + var package = toLoad[i]; + loadingRange = new Range(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length); + foreach (var progress in package.LoadPackageEnumerable()) + { + if (progress.Exception != null) + { + //If an exception was thrown while loading this package, refuse to add it to the list of enabled packages + newRegular.Remove(package); + break; + } + yield return progress.Transform(loadingRange); + } + } + regular.Clear(); regular.AddRange(newRegular); + SortContent(); + yield return new LoadProgress(1.0f); + } + + public static void ThrowIfDuplicates(IEnumerable pkgs) + { + var contentPackages = pkgs as IList ?? pkgs.ToArray(); + if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2))) + { + throw new InvalidOperationException($"Input contains duplicate packages"); + } + } + + private class TypeComparer : IEqualityComparer + { + public bool Equals([AllowNull] T x, [AllowNull] T y) + { + if (x is null || y is null) + { + return x is null == y is null; + } + return x.GetType() == y.GetType(); + } + + public int GetHashCode([DisallowNull] T obj) + { + return obj.GetType().GetHashCode(); + } + } + + public static void SortContent() + { + ThrowIfDuplicates(All); + All + .SelectMany(r => r.Files) + .Distinct(new TypeComparer()) + .ForEach(f => f.Sort()); + } + + public static int IndexOf(ContentPackage contentPackage) + { + if (contentPackage is CorePackage core) + { + if (core == Core) { return 0; } + return -1; + } + else if (contentPackage is RegularPackage reg) + { + return Regular.IndexOf(reg) + 1; + } + return -1; + } + + public static void DisableRemovedMods() + { + if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) + { + SetCore(ContentPackageManager.CorePackages.First()); + } + SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray()); + } + + public static void BackUp() + { + if (BackupPackages.Core != null || BackupPackages.Regular != null) + { + throw new InvalidOperationException("Tried to back up enabled packages multiple times"); + } + + BackupPackages.Core = Core; + BackupPackages.Regular = Regular.ToImmutableArray(); + } + + public static void Restore() + { + if (BackupPackages.Core == null || BackupPackages.Regular == null) + { + DebugConsole.AddWarning("Tried to restore enabled packages multiple times/without performing a backup"); + return; + } + + SetCore(BackupPackages.Core); + SetRegular(BackupPackages.Regular); + + BackupPackages.Core = null; + BackupPackages.Regular = null; + } + } + + public sealed partial class PackageSource : ICollection + { + private readonly Predicate? skipPredicate; + + public PackageSource(string dir, Predicate? skipPredicate) + { + this.skipPredicate = skipPredicate; + directory = dir; + Directory.CreateDirectory(directory); + } + + public void SwapPackage(ContentPackage oldPackage, ContentPackage newPackage) + { + bool contains = false; + if (oldPackage is CorePackage oldCore && corePackages.Contains(oldCore)) + { + corePackages.Remove(oldCore); + contains = true; + } + else if (oldPackage is RegularPackage oldRegular && regularPackages.Contains(oldRegular)) + { + regularPackages.Remove(oldRegular); + contains = true; + } + + if (contains) + { + if (newPackage is CorePackage newCore) + { + corePackages.Add(newCore); + } + else if (newPackage is RegularPackage newRegular) + { + regularPackages.Add(newRegular); + } + } + } + + public void Refresh() + { + //remove packages that have been deleted from the directory + corePackages.RemoveWhere(p => !File.Exists(p.Path)); + regularPackages.RemoveWhere(p => !File.Exists(p.Path)); + + //load packages that have been added to the directory + var subDirs = Directory.GetDirectories(directory); + foreach (string subDir in subDirs) + { + var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform(); + if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; } + if (File.Exists(fileListPath)) + { + if (skipPredicate?.Invoke(fileListPath) is true) { continue; } + + ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath); + if (newPackage is CorePackage corePackage) + { + corePackages.Add(corePackage); + } + else if (newPackage is RegularPackage regularPackage) + { + regularPackages.Add(regularPackage); + } + + if (!(newPackage is null)) + { + Debug.WriteLine($"Loaded \"{newPackage.Name}\""); + } + } + } + } + + private readonly string directory; + private readonly HashSet regularPackages = new HashSet(); + public IEnumerable Regular => regularPackages; + + private readonly HashSet corePackages = new HashSet(); + public IEnumerable Core => corePackages; + + public IEnumerator GetEnumerator() + { + foreach (var core in Core) { yield return core; } + foreach (var regular in Regular) { yield return regular; } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void ICollection.Add(ContentPackage item) => throw new InvalidOperationException(); + + void ICollection.Clear() => throw new InvalidOperationException(); + + public bool Contains(ContentPackage item) + => item switch + { + CorePackage core => corePackages.Contains(core), + RegularPackage regular => this.regularPackages.Contains(regular), + _ => throw new ArgumentException($"Expected regular or core package, got {item.GetType().Name}") + }; + + void ICollection.CopyTo(ContentPackage[] array, int arrayIndex) + { + foreach (var package in corePackages) + { + array[arrayIndex] = package; + arrayIndex++; + } + + foreach (var package in regularPackages) + { + array[arrayIndex] = package; + arrayIndex++; + } + } + + bool ICollection.Remove(ContentPackage item) => throw new InvalidOperationException(); + + public int Count => corePackages.Count + regularPackages.Count; + public bool IsReadOnly => true; + } + + public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null); + public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath); + + public static CorePackage? VanillaCorePackage { get; private set; } = null; + + public static IEnumerable CorePackages + => (VanillaCorePackage is null + ? Enumerable.Empty() + : VanillaCorePackage.ToEnumerable()) + .CollectionConcat(LocalPackages.Core.CollectionConcat(WorkshopPackages.Core)); + + public static IEnumerable RegularPackages + => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular); + + public static IEnumerable AllPackages + => LocalPackages.CollectionConcat(WorkshopPackages); + + public static void UpdateContentPackageList() + { + LocalPackages.Refresh(); + WorkshopPackages.Refresh(); + EnabledPackages.DisableRemovedMods(); + } + + public static ContentPackage? ReloadContentPackage(ContentPackage p) + { + ContentPackage? newPackage = ContentPackage.TryLoad(p.Path); + if (newPackage is CorePackage core) + { + if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); } + } + else if (newPackage is RegularPackage regular) + { + int index = EnabledPackages.Regular.IndexOf(p); + if (index >= 0) + { + var newRegular = EnabledPackages.Regular.ToArray(); + newRegular[index] = regular; + EnabledPackages.SetRegular(newRegular); + } + } + + if (newPackage != null) + { + LocalPackages.SwapPackage(p, newPackage); + WorkshopPackages.SwapPackage(p, newPackage); + } + EnabledPackages.DisableRemovedMods(); + return newPackage; + } + + public readonly struct LoadProgress + { + public readonly float Value; + public readonly Exception? Exception; + + public LoadProgress(float value) + { + Value = value; + Exception = null; + } + + private LoadProgress(Exception exception) + { + Value = -1f; + Exception = exception; + } + + public static LoadProgress Failure(Exception exception) + => new LoadProgress(exception); + + public LoadProgress Transform(Range range) + => Exception != null + ? this + : new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value)); + } + + public static void LoadVanillaFileList() + { + VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); + } + + public static IEnumerable Init() + { + Range loadingRange = new Range(0.0f, 1.0f); + + SteamManager.Workshop.DeleteFailedCopies(); + UpdateContentPackageList(); + + if (VanillaCorePackage is null) { LoadVanillaFileList(); } + + CorePackage enabledCorePackage = VanillaCorePackage!; + List enabledRegularPackages = new List(); + +#if CLIENT + TaskPool.Add("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { }); +#else + #warning TODO: implement Workshop updates for servers at some point +#endif + + var contentPackagesElement = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath)?.Root + ?.GetChildElement("ContentPackages"); + if (contentPackagesElement != null) + { + T? findPackage(IEnumerable packages, XElement? elem) where T : ContentPackage + { + if (elem is null) { return null; } + string name = elem.GetAttributeString("name", ""); + string path = elem.GetAttributeStringUnrestricted("path", "").CleanUpPathCrossPlatform(correctFilenameCase: false); + return + packages.FirstOrDefault(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase)) + ?? packages.FirstOrDefault(p => p.NameMatches(name)); + } + + var corePackageElement = contentPackagesElement.GetChildElement(CorePackageElementName); + enabledCorePackage = findPackage(CorePackages, corePackageElement) ?? VanillaCorePackage!; + + var regularPackagesElement = contentPackagesElement.GetChildElement(RegularPackagesElementName); + if (regularPackagesElement != null) + { + XElement[] regularPackageElements = regularPackagesElement.GetChildElements(RegularPackagesSubElementName).ToArray(); + for (int i = 0; i < regularPackageElements.Length; i++) + { + var regularPackage = findPackage(RegularPackages, regularPackageElements[i]); + if (regularPackage != null) { enabledRegularPackages.Add(regularPackage); } + } + } + } + + int pkgCount = 1 + enabledRegularPackages.Count; //core + regular + + loadingRange = new Range(0.01f, 1.0f / pkgCount); + foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage)) + { + yield return p.Transform(loadingRange); + } + + loadingRange = new Range(1.0f / pkgCount, 1.0f); + foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages)) + { + yield return p.Transform(loadingRange); + } + + yield return new LoadProgress(1.0f); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs new file mode 100644 index 000000000..3d0e13e7a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -0,0 +1,149 @@ +#nullable enable + +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Barotrauma.IO; + +namespace Barotrauma +{ + public sealed class ContentPath + { + public readonly static ContentPath Empty = new ContentPath(null, ""); + + public const string ModDirStr = "%ModDir%"; + public const string OtherModDirFmt = "%ModDir:{0}%"; + private static readonly Regex OtherModDirRegex = new Regex( + string.Format(OtherModDirFmt, "(.+?)")); + + public readonly string? RawValue; + + public readonly ContentPackage? ContentPackage; + + private string? cachedValue; + private string? cachedFullPath; + + public string Value + { + get + { + if (RawValue.IsNullOrEmpty()) { return ""; } + if (!cachedValue.IsNullOrEmpty()) { return cachedValue!; } + + string? modName = ContentPackage?.Name; + + var otherMods = OtherModDirRegex.Matches(RawValue ?? throw new NullReferenceException($"{nameof(RawValue)} is null.")) + .Select(m => m.Groups[1].Value.Trim().ToIdentifier()) + .Distinct().Where(id => !id.IsEmpty && id != modName).ToHashSet(); + cachedValue = RawValue!; + if (!(ContentPackage is null)) + { + string modPath = Path.GetDirectoryName(ContentPackage.Path)!; + cachedValue = cachedValue + .Replace(ModDirStr, modPath, StringComparison.OrdinalIgnoreCase) + .Replace(string.Format(OtherModDirFmt, ContentPackage.Name), modPath, StringComparison.OrdinalIgnoreCase); + if (ContentPackage.SteamWorkshopId != 0) + { + cachedValue = cachedValue + .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); + } + } + var allPackages = ContentPackageManager.AllPackages; + foreach (Identifier otherModName in otherMods) + { + if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; } + ContentPackage? otherMod = + allPackages.FirstOrDefault(p => workshopId != 0 && p.SteamWorkshopId != 0 && workshopId == p.SteamWorkshopId) + ?? allPackages.FirstOrDefault(p => p.Name == otherModName) + ?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName)) + ?? throw new MissingContentPackageException(ContentPackage, otherModName.Value); + cachedValue = cachedValue.Replace(string.Format(OtherModDirFmt, otherModName.Value), Path.GetDirectoryName(otherMod.Path)); + } + cachedValue = cachedValue.CleanUpPath(); + return cachedValue; + } + } + + public string FullPath + { + get + { + if (cachedFullPath.IsNullOrEmpty()) + { + if (Value.IsNullOrEmpty()) + { + return ""; + } + cachedFullPath = Path.GetFullPath(Value).CleanUpPathCrossPlatform(correctFilenameCase: false); + } + return cachedFullPath!; + } + } + + private ContentPath(ContentPackage? contentPackage, string? rawValue) + { + ContentPackage = contentPackage; + RawValue = rawValue; + cachedValue = null; + cachedFullPath = null; + } + + public static ContentPath FromRaw(string? rawValue) + => new ContentPath(null, rawValue); + + public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue) + => new ContentPath(contentPackage, rawValue); + + public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue) + { + throw new NotImplementedException(); + } + + private static bool StringEquality(string? a, string? b) + => (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || + string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), + Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), + StringComparison.OrdinalIgnoreCase); + + public static bool operator==(ContentPath a, ContentPath b) + => StringEquality(a.Value, b.Value); + + public static bool operator!=(ContentPath a, ContentPath b) => !(a == b); + + public static bool operator==(ContentPath a, string? b) + => StringEquality(a.Value, b); + + public static bool operator!=(ContentPath a, string? b) => !(a == b); + + public static bool operator==(string? a, ContentPath b) + => StringEquality(a, b.Value); + + public static bool operator!=(string? a, ContentPath b) => !(a == b); + + protected bool Equals(ContentPath other) + { + return RawValue == other.RawValue && Equals(ContentPackage, other.ContentPackage) && cachedValue == other.cachedValue && cachedFullPath == other.cachedFullPath; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ContentPath)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(RawValue, ContentPackage, cachedValue, cachedFullPath); + } + + public bool IsNullOrEmpty() => string.IsNullOrEmpty(Value); + public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value); + + public bool EndsWith(string suffix) => Value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); + + public override string? ToString() => Value; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs new file mode 100644 index 000000000..60f048a4e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -0,0 +1,130 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Xml.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public sealed class ContentXElement + { + public ContentPackage? ContentPackage { get; private set; } + public readonly XElement Element; + + public ContentXElement(ContentPackage? contentPackage, XElement element) + { + ContentPackage = contentPackage; + Element = element; + } + + public static implicit operator XElement?(ContentXElement? cxe) => cxe?.Element; + //public static implicit operator ContentXElement?(XElement? xe) => xe is null ? null : new ContentXElement(null, xe); + + public XName Name => Element.Name; + public Identifier NameAsIdentifier() => Element.NameAsIdentifier(); + + public string BaseUri => Element.BaseUri; + + public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null."); + + public ContentXElement? FirstElement() => Elements().FirstOrDefault(); + + public ContentXElement? Parent => Element.Parent is null ? null : new ContentXElement(ContentPackage, Element.Parent); + public bool HasElements => Element.HasElements; + + public bool IsOverride() => Element.IsOverride(); + + public bool ComesAfter(ContentXElement other) => Element.ComesAfter(other.Element); + + public ContentXElement? GetChildElement(string name) + => Element.GetChildElement(name) is { } elem ? new ContentXElement(ContentPackage, elem) : null; + + public IEnumerable Elements() + => Element.Elements().Select(e => new ContentXElement(ContentPackage, e)); + + public IEnumerable ElementsBeforeSelf() + => Element.ElementsBeforeSelf().Select(e => new ContentXElement(ContentPackage, e)); + + public IEnumerable Descendants() + => Element.Descendants().Select(e => new ContentXElement(ContentPackage, e)); + + public IEnumerable GetChildElements(string name) + => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase)); + + public XAttribute? Attribute(string name) => Element.Attribute(name); + + public XAttribute? GetAttribute(string name) => Element.GetAttribute(name); + + public IEnumerable Attributes() => Element.Attributes(); + public IEnumerable Attributes(string name) => Element.Attributes(name); + + public string ElementInnerText() => Element.ElementInnerText(); + + public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); + public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); + public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); + public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); + public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); + public ContentPath? GetAttributeContentPath(string key) => Element.GetAttributeContentPath(key, ContentPackage); + public int GetAttributeInt(string key, int def) => Element.GetAttributeInt(key, def); + public int[]? GetAttributeIntArray(string key, int[]? def) => Element.GetAttributeIntArray(key, def); + public ushort[]? GetAttributeUshortArray(string key, ushort[]? def) => Element.GetAttributeUshortArray(key, def); + public float GetAttributeFloat(string key, float def) => Element.GetAttributeFloat(key, def); + public float[]? GetAttributeFloatArray(string key, float[]? def) => Element.GetAttributeFloatArray(key, def); + public float GetAttributeFloat(float def, params string[] keys) => Element.GetAttributeFloat(def, keys); + public bool GetAttributeBool(string key, bool def) => Element.GetAttributeBool(key, def); + public Point GetAttributePoint(string key, in Point def) => Element.GetAttributePoint(key, def); + public Vector2 GetAttributeVector2(string key, in Vector2 def) => Element.GetAttributeVector2(key, def); + public Vector4 GetAttributeVector4(string key, in Vector4 def) => Element.GetAttributeVector4(key, def); + public Color GetAttributeColor(string key, in Color def) => Element.GetAttributeColor(key, def); + public Color? GetAttributeColor(string key) => Element.GetAttributeColor(key); + public Color[]? GetAttributeColorArray(string key, Color[]? def) => Element.GetAttributeColorArray(key, def); + public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def); + public T GetAttributeEnum(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); + public (T1, T2) GetAttributeTuple(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); + public (T1, T2)[] GetAttributeTupleArray(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); + + public Identifier VariantOf() => Element.VariantOf(); + + public bool DoesAttributeReferenceFileNameAlone(string key) => Element.DoesAttributeReferenceFileNameAlone(key); + + public string ParseContentPathFromUri() => Element.ParseContentPathFromUri(); + + public void SetAttributeValue(string key, string val) => Element.SetAttributeValue(key, val); + + public void Add(ContentXElement elem) + { + Element.Add(elem.Element); + elem.ContentPackage = ContentPackage; + #warning TODO: update %ModDir% instances in case the content package changes + } + + public void AddFirst(ContentXElement elem) + { + Element.AddFirst(elem.Element); + elem.ContentPackage = ContentPackage; + #warning TODO: update %ModDir% instances in case the content package changes + } + + public void AddAfterSelf(ContentXElement elem) + { + Element.AddAfterSelf(elem.Element); + elem.ContentPackage = ContentPackage; + #warning TODO: update %ModDir% instances in case the content package changes + } + + public void Remove() => Element.Remove(); + } + + public static class ContentXElementExtensions + { + public static ContentXElement FromPackage(this XElement element, ContentPackage? contentPackage) + => new ContentXElement(contentPackage, element); + + public static IEnumerable Elements(this IEnumerable elements) + => elements.SelectMany(e => e.Elements()); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs new file mode 100644 index 000000000..b19d0f034 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs @@ -0,0 +1,160 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Barotrauma +{ + // Identifier struct to eliminate case-sensitive comparisons + public readonly struct Identifier : IComparable, IEquatable + { + public readonly static Identifier Empty = default; + + private readonly static int emptyHash = "".GetHashCode(StringComparison.OrdinalIgnoreCase); + + private readonly string? value; + private readonly Lazy? hashCode; + + public string Value => value ?? ""; + public int HashCode => hashCode?.Value ?? emptyHash; + + public Identifier(string? str) + { + value = str; + hashCode = new Lazy(() => (str ?? "").GetHashCode(StringComparison.OrdinalIgnoreCase)); + } + + public bool IsEmpty => Value.IsNullOrEmpty(); + + public Identifier IfEmpty(in Identifier id) + => IsEmpty ? id : this; + + public Identifier Replace(in Identifier subStr, in Identifier newStr) + => Replace(subStr.Value ?? "", newStr.Value ?? ""); + + public Identifier Replace(string subStr, string newStr) + => (Value?.Replace(subStr, newStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + + public Identifier Remove(Identifier subStr) + => Remove(subStr.Value ?? ""); + + public Identifier Remove(string subStr) + => (Value?.Remove(subStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + + public override bool Equals(object? obj) => + obj switch + { + Identifier i => this == i, + string s => this == s, + _ => base.Equals(obj) + }; + + public bool StartsWith(string str) => Value?.StartsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + + public bool StartsWith(Identifier id) => StartsWith(id.Value ?? ""); + + public bool EndsWith(string str) => Value?.EndsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + + public bool EndsWith(Identifier id) => EndsWith(id.Value ?? ""); + + public Identifier AppendIfMissing(string suffix) + => EndsWith(suffix) ? this : $"{this}{suffix}".ToIdentifier(); + + public Identifier RemoveFromEnd(string suffix) + => (Value?.RemoveFromEnd(suffix, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + + public bool Contains(string str) => Value?.Contains(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + + public bool Contains(in Identifier id) => Contains(id.Value ?? ""); + + public override string ToString() => Value ?? ""; + + public override int GetHashCode() => HashCode; + + public int CompareTo(object? obj) + { + return string.Compare(Value, obj?.ToString() ?? "", StringComparison.InvariantCultureIgnoreCase); + } + + public bool Equals([AllowNull] Identifier other) + { + return this == other; + } + + private static bool StringEquality(string? a, string? b) + => (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || string.Equals(a, b, StringComparison.OrdinalIgnoreCase); + + public static bool operator ==(in Identifier a, in Identifier b) => + StringEquality(a.Value, b.Value); + + public static bool operator !=(in Identifier a, in Identifier b) => + !(a == b); + + public static bool operator ==(in Identifier identifier, string? str) => + StringEquality(identifier.Value, str); + + public static bool operator !=(in Identifier identifier, string? str) => + !(identifier == str); + + public static bool operator ==(string? str, in Identifier identifier) => + identifier == str; + + public static bool operator !=(string? str, in Identifier identifier) => + !(identifier == str); + + public static bool operator ==(in Identifier? a, in Identifier? b) => + StringEquality(a?.Value, b?.Value); + + public static bool operator !=(in Identifier? a, in Identifier? b) => + !(a == b); + + public static bool operator ==(in Identifier? a, string? b) => + StringEquality(a?.Value, b); + + public static bool operator !=(in Identifier? a, string? b) => + !(a == b); + + public static bool operator ==(string str, in Identifier? identifier) => + identifier == str; + + public static bool operator !=(string str, in Identifier? identifier) => + !(identifier == str); + } + + public static class IdentifierExtensions + { + public static IEnumerable ToIdentifiers(this IEnumerable strings) + { + foreach (string s in strings) + { + if (string.IsNullOrEmpty(s)) { continue; } + yield return new Identifier(s); + } + } + + public static Identifier[] ToIdentifiers(this string[] strings) + => ((IEnumerable)strings).ToIdentifiers().ToArray(); + + public static Identifier ToIdentifier(this string? s) + { + return new Identifier(s); + } + + public static Identifier ToIdentifier(this T t) where T: notnull + { + return t.ToString().ToIdentifier(); + } + + public static bool Contains(this ISet set, string identifier) + { + return set.Contains(identifier.ToIdentifier()); + } + + public static bool ContainsKey(this IReadOnlyDictionary dictionary, string key) + { + return dictionary.ContainsKey(key.ToIdentifier()); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs new file mode 100644 index 000000000..2c6d0df5e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs @@ -0,0 +1,17 @@ +#nullable enable +using System; + +namespace Barotrauma +{ + public sealed class MissingContentPackageException : Exception + { + public override string Message { get; } + + public MissingContentPackageException(ContentPackage? whoAsked, string? missingPackage) + { + Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " + + $"with name or ID \"{missingPackage ?? "[NULL]"}\" " + + $"that is not currently installed."; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs new file mode 100644 index 000000000..e69de29bb diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs deleted file mode 100644 index dfcd2b32d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ /dev/null @@ -1,875 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.Steam; - -namespace Barotrauma -{ - public enum ContentType - { - None, - Submarine, - Jobs, - Item, - ItemAssembly, - Character, - Structure, - Outpost, - OutpostModule, - OutpostConfig, - BeaconStation, - NPCSets, - Factions, - Text, - ServerExecutable, - LocationTypes, - MapGenerationParameters, - LevelGenerationParameters, - CaveGenerationParameters, - LevelObjectPrefabs, - RandomEvents, - Missions, - BackgroundCreaturePrefabs, - Sounds, - RuinConfig, - Particles, - Decals, - NPCConversations, - Afflictions, - Tutorials, - UIStyle, - TraitorMissions, - EventManagerSettings, - Orders, - SkillSettings, - Wreck, - Corpses, - WreckAIConfig, - UpgradeModules, - MapCreature, - EnemySubmarine, - Talents, - TalentTrees, - } - - public class ContentPackage - { - public static string Folder = "Data/ContentPackages/"; - - private static readonly List regularPackages = new List(); - public static IReadOnlyList RegularPackages - { - get { return regularPackages; } - } - - private static readonly List corePackages = new List(); - public static IReadOnlyList CorePackages - { - get { return corePackages; } - } - - public static IEnumerable AllPackages - { - get { return corePackages.Concat(regularPackages); } - } - - //these types of files are included in the MD5 hash calculation, - //meaning that the players must have the exact same files to play together - public static HashSet MultiplayerIncompatibleContent { get; private set; } = new HashSet - { - ContentType.Jobs, - ContentType.Item, - ContentType.Character, - ContentType.Structure, - ContentType.LocationTypes, - ContentType.NPCSets, - ContentType.Factions, - ContentType.MapGenerationParameters, - ContentType.LevelGenerationParameters, - ContentType.CaveGenerationParameters, - ContentType.Missions, - ContentType.LevelObjectPrefabs, - ContentType.RuinConfig, - ContentType.Outpost, - ContentType.OutpostModule, - ContentType.OutpostConfig, - ContentType.Wreck, - ContentType.WreckAIConfig, - ContentType.BeaconStation, - ContentType.Afflictions, - ContentType.Orders, - ContentType.Corpses, - ContentType.UpgradeModules, - ContentType.MapCreature, - ContentType.EnemySubmarine, - ContentType.Talents, - }; - - //at least one file of each these types is required in core content packages - private static readonly HashSet corePackageRequiredFiles = new HashSet - { - ContentType.Jobs, - ContentType.Item, - ContentType.Character, - ContentType.Structure, - //TODO: there needs to be either outpost files or outpost generation parameters, both aren't required - //ContentType.Outpost, - //ContentType.OutpostGenerationParams, - ContentType.Factions, - ContentType.Wreck, - ContentType.WreckAIConfig, - ContentType.BeaconStation, - ContentType.Text, - ContentType.ServerExecutable, - ContentType.LocationTypes, - ContentType.MapGenerationParameters, - ContentType.LevelGenerationParameters, - ContentType.CaveGenerationParameters, - ContentType.RandomEvents, - ContentType.Missions, - ContentType.RuinConfig, - ContentType.Afflictions, - ContentType.UIStyle, - ContentType.EventManagerSettings, - ContentType.Orders, - ContentType.Corpses, - ContentType.UpgradeModules, - ContentType.EnemySubmarine, - ContentType.Talents, - }; - - public static IEnumerable CorePackageRequiredFiles - { - get { return corePackageRequiredFiles; } - } - - public static bool IngameModSwap = false; - - public string Name { get; set; } = string.Empty; - - public string Path - { - get; - set; - } - - public ulong SteamWorkshopId; - public DateTime? InstallTime; - - public bool HideInWorkshopMenu - { - get; - private set; - } - - private Md5Hash md5Hash; - public Md5Hash MD5hash - { - get - { - if (md5Hash == null) - { - //TODO: before re-enabling content package hash caching, make sure the hash gets recalculated when any file in the content package changes, not just when the filelist.xml changes. - /*md5Hash = Md5Hash.FetchFromCache(Path); - if (md5Hash == null) - { - CalculateHash(); - md5Hash.SaveToCache(Path); - }*/ - CalculateHash(); - } - return md5Hash; - } - } - - //core packages are content packages that are required for the game to work - //e.g. they include the executable, some location types, level generation params and other files the game won't work without - //one (and only one) core package must always be selected - private bool isCorePackage; - public bool IsCorePackage - { - get { return isCorePackage; } - set - { - isCorePackage = value; - if (isCorePackage && regularPackages.Contains(this)) - { - corePackages.AddOnMainThread(this); - regularPackages.RemoveOnMainThread(this); - } - else if (!isCorePackage && corePackages.Contains(this)) - { - regularPackages.AddOnMainThread(this); - corePackages.RemoveOnMainThread(this); - } - } - } - - public Version GameVersion - { - get; set; - } - - - private readonly List files; - private readonly List filesToAdd; - private readonly List filesToRemove; - - public IReadOnlyList Files - { - get { return files; } - } - - public IEnumerable FilesUnsaved - { - get { return files.Where(f => !filesToRemove.Contains(f)).Concat(filesToAdd); } - } - - public IReadOnlyList FilesToAdd - { - get { return filesToAdd; } - } - - public IReadOnlyList FilesToRemove - { - get { return filesToRemove; } - } - - public bool HasMultiplayerIncompatibleContent - { - get { return Files.Any(f => MultiplayerIncompatibleContent.Contains(f.Type)); } - } - - public bool IsCorrupt - { - get; - private set; - } - - private ContentPackage() - { - files = new List(); - filesToAdd = new List(); - filesToRemove = new List(); - } - - public ContentPackage(string filePath, string setPath = "") - : this() - { - filePath = filePath.CleanUpPath(); - if (!string.IsNullOrEmpty(setPath)) { setPath = setPath.CleanUpPath(); } - XDocument doc = XMLExtensions.TryLoadXml(filePath); - - Path = setPath == string.Empty ? filePath : setPath; - - if (doc?.Root == null) - { - DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!"); - IsCorrupt = true; - return; - } - - Name = doc.Root.GetAttributeString("name", ""); - HideInWorkshopMenu = doc.Root.GetAttributeBool("hideinworkshopmenu", false); - isCorePackage = doc.Root.GetAttributeBool("corepackage", false); - SteamWorkshopId = doc.Root.GetAttributeUInt64("steamworkshopid", 0); - string workshopUrl = doc.Root.GetAttributeString("steamworkshopurl", ""); - if (!string.IsNullOrEmpty(workshopUrl)) - { - SteamWorkshopId = SteamManager.GetWorkshopItemIDFromUrl(workshopUrl); - } - string versionStr = doc.Root.GetAttributeString("gameversion", "0.0.0.0"); - try - { - GameVersion = new Version(versionStr); - } - catch - { - DebugConsole.ThrowError($"Invalid version number in content package \"{Name}\" ({versionStr})."); - GameVersion = GameMain.Version; - } - if (doc.Root.Attribute("installtime") != null) - { - InstallTime = ToolBox.Epoch.ToDateTime(doc.Root.GetAttributeUInt("installtime", 0)); - } - - List errorMsgs = new List(); - foreach (XElement subElement in doc.Root.Elements()) - { - if (subElement.Name.ToString().Equals("executable", StringComparison.OrdinalIgnoreCase)) { continue; } - if (!Enum.TryParse(subElement.Name.ToString(), true, out ContentType type)) - { - errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type."); - type = ContentType.None; - } - files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this)); - } - - if (Files.Count == 0) - { - //no files defined, find a submarine in here - //because somehow people have managed to upload - //mods without contentfile definitions - string folder = System.IO.Path.GetDirectoryName(filePath); - if (File.Exists(System.IO.Path.Combine(folder, Name+".sub"))) - { - files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this)); - } - else - { - errorMsgs.Add("Error in content package \"" + Name + "\" - no content files defined."); - } - } - - bool compatible = IsCompatible(); - //If we know that the package is not compatible, don't display error messages. - if (compatible) - { - foreach (string errorMsg in errorMsgs) - { - DebugConsole.ThrowError(errorMsg); - } - } - } - - private bool? hasErrors; - public bool HasErrors - { - get - { - if (!hasErrors.HasValue) - { - hasErrors = !CheckErrors(out _); - } - return hasErrors.Value; - } - } - - private List errorMessages; - public IEnumerable ErrorMessages - { - get - { - if (errorMessages == null) { CheckErrors(out _); } - return errorMessages; - } - } - - public override string ToString() - { - return Name; - } - - public bool IsCompatible() - { - if (Files.All(f => f.Type == ContentType.Submarine)) - { - return true; - } - - //content package compatibility checks were added in 0.8.9.1 - //v0.8.9.1 is not compatible with older content packages - if (GameVersion < new Version(0, 8, 9, 1)) - { - return false; - } - - //do additional checks here if later versions add changes that break compatibility - - return true; - } - - public bool ContainsRequiredCorePackageFiles() - { - return corePackageRequiredFiles.All(fileType => Files.Any(file => file.Type == fileType)); - } - - public bool ContainsRequiredCorePackageFiles(out List missingContentTypes) - { - missingContentTypes = new List(); - foreach (ContentType contentType in corePackageRequiredFiles) - { - if (!Files.Any(file => file.Type == contentType)) - { - missingContentTypes.Add(contentType); - } - } - return missingContentTypes.Count == 0; - } - - public bool CheckErrors(out List errorMessages) - { - this.errorMessages = errorMessages = new List(); - foreach (ContentFile file in Files) - { - switch (file.Type) - { - case ContentType.ServerExecutable: - case ContentType.None: - case ContentType.Outpost: - case ContentType.OutpostModule: - case ContentType.Submarine: - case ContentType.Wreck: - case ContentType.BeaconStation: - case ContentType.EnemySubmarine: - break; - default: - try - { - using FileStream stream = File.Open(file.Path, System.IO.FileMode.Open, System.IO.FileAccess.Read); - using var reader = XMLExtensions.CreateReader(stream); - XDocument.Load(reader); - } - catch (Exception e) - { - if (TextManager.Initialized) - { - errorMessages.Add(TextManager.GetWithVariables("xmlfileinvalid", - new string[] { "[filepath]", "[errormessage]" }, - new string[] { file.Path, e.Message })); - } - else - { - errorMessages.Add($"XML File Invalid. PATH: {file.Path}, ERROR: {e.Message}"); -#if DEBUG - throw; -#endif - } - } - break; - } - } - - if (IsCorePackage && !ContainsRequiredCorePackageFiles(out List missingContentTypes)) - { - errorMessages.Add(TextManager.GetWithVariables("ContentPackageCantMakeCorePackage", - new string[2] { "[packagename]", "[missingfiletypes]" }, - new string[2] { Name, string.Join(", ", missingContentTypes) }, - new bool[2] { false, true })); - } - VerifyFiles(out List missingFileMessages); - - errorMessages.AddRange(missingFileMessages); - hasErrors = errorMessages.Count > 0; - return !hasErrors.Value; - } - - /// - /// Make sure all the files defined in the content package are present - /// - /// - public bool VerifyFiles(out List errorMessages) - { - errorMessages = new List(); - foreach (ContentFile file in Files) - { - //TODO: determine executable extension on platform and check for the presence of the executables - if (file.Type == ContentType.ServerExecutable) { continue; } - - if (!File.Exists(file.Path)) - { - errorMessages.Add("File \"" + file.Path + "\" not found."); - continue; - } - } - - return errorMessages.Count == 0; - } - - public static ContentPackage CreatePackage(string name, string path, bool corePackage) - { - ContentPackage newPackage = new ContentPackage() - { - Name = name, - Path = path, - isCorePackage = corePackage, - GameVersion = GameMain.Version - }; - - return newPackage; - } - - public ContentFile AddFile(string path, ContentType type) - { - if (Files.Concat(FilesToAdd).Any(file => file.Path == path && file.Type == type)) return null; - - ContentFile cf = new ContentFile(path, type) - { - ContentPackage = this - }; - filesToAdd.Add(cf); - - return cf; - } - - public void AddFile(ContentFile file) - { - if (filesToRemove.Contains(file)) { filesToRemove.Remove(file); } - if (Files.Concat(FilesToAdd).Any(f => f.Path == file.Path && f.Type == file.Type)) return; - - filesToAdd.Add(file); - } - - public void RemoveFile(ContentFile file) - { - if (filesToAdd.Contains(file)) { filesToAdd.Remove(file); } - if (files.Contains(file) && !filesToRemove.Contains(file)) { filesToRemove.Add(file); } - } - - public void Save(string filePath, bool reload = true) - { - var packagesToDeselect = corePackages.Concat(regularPackages).Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList(); - bool refreshFiles = false; - - if (packagesToDeselect.Any()) - { - foreach (var p in packagesToDeselect) - { - if (p.IsCorePackage) - { - if (GameMain.Config.CurrentCorePackage == p) - { - refreshFiles = true; - } - corePackages.RemoveOnMainThread(p); - } - else - { - if (GameMain.Config.EnabledRegularPackages.Contains(p)) - { - refreshFiles = true; - } - regularPackages.RemoveOnMainThread(p); - } - } - if (IsCorePackage) - { - corePackages.AddOnMainThread(this); - } - else - { - regularPackages.AddOnMainThread(this); - } - - if (refreshFiles) - { - GameMain.Config.DisableContentPackageItems(filesToRemove); - GameMain.Config.EnableContentPackageItems(filesToAdd); - GameMain.Config.RefreshContentPackageItems(filesToRemove.Concat(filesToAdd).Distinct()); - } - } - files.RemoveAll(f => filesToRemove.Contains(f)); - files.AddRange(filesToAdd); - filesToRemove.Clear(); filesToAdd.Clear(); - - XDocument doc = new XDocument(); - doc.Add(new XElement("contentpackage", - new XAttribute("name", Name), - new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)), - new XAttribute("corepackage", IsCorePackage))); - - doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString())); - - if (SteamWorkshopId != 0) - { - doc.Root.Add(new XAttribute("steamworkshopid", SteamWorkshopId.ToString())); - } - - if (InstallTime != null) - { - doc.Root.Add(new XAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value))); - } - - foreach (ContentFile file in Files) - { - doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path.CleanUpPathCrossPlatform()))); - } - - doc.SaveSafe(filePath); - } - - public void CalculateHash(bool logging = false) - { - List hashes = new List(); - - if (logging) - { - DebugConsole.NewMessage("****************************** Calculating cp hash " + Name); - } - - foreach (ContentFile file in Files) - { - if (!MultiplayerIncompatibleContent.Contains(file.Type)) { continue; } - - try - { - var hash = CalculateFileHash(file); - if (logging) - { - var fileMd5 = new Md5Hash(hash); - DebugConsole.NewMessage(" " + file.Path + ": " + fileMd5.Hash); - } - hashes.Add(hash); - } - - catch (Exception e) - { - DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e); - break; - } - } - - byte[] bytes = new byte[hashes.Count * 16]; - for (int i = 0; i < hashes.Count; i++) - { - hashes[i].CopyTo(bytes, i * 16); - } - - md5Hash = new Md5Hash(bytes); - if (logging) - { - DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.Hash); - } - } - - private byte[] CalculateFileHash(ContentFile file) - { - using (MD5 md5 = MD5.Create()) - { - List filePaths = new List { file.Path }; - List data = new List(); - - switch (file.Type) - { - case ContentType.Character: - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - var ragdollFolder = RagdollParams.GetFolder(doc, file.Path); - if (Directory.Exists(ragdollFolder)) - { - Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f)); - } - var animationFolder = AnimationParams.GetFolder(doc, file.Path); - if (Directory.Exists(animationFolder)) - { - Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f)); - } - break; - } - - if (filePaths.Count > 1) - { - using (MD5 tempMd5 = MD5.Create()) - { - filePaths = filePaths.OrderBy(f => ToolBox.StringToUInt32Hash(f.CleanUpPathCrossPlatform(true).ToLowerInvariant(), tempMd5)).ToList(); - } - } - - foreach (string filePath in filePaths) - { - if (!File.Exists(filePath)) continue; - - using (var stream = File.OpenRead(filePath)) - { - byte[] fileData = new byte[stream.Length]; - stream.Read(fileData, 0, (int)stream.Length); - if (filePath.EndsWith(".xml", true, System.Globalization.CultureInfo.InvariantCulture)) - { - string text = System.Text.Encoding.UTF8.GetString(fileData); - text = text.Replace("\n", "").Replace("\r", "").Replace("\\","/"); - fileData = System.Text.Encoding.UTF8.GetBytes(text); - } - data.AddRange(fileData); - } - } - return md5.ComputeHash(data.ToArray()); - } - } - - public static bool IsModFilePathAllowed(ContentFile contentFile) - { - string path = contentFile.Path; - return IsModFilePathAllowed(path); - } - /// - /// Returns whether mods are allowed to install a file into the specified path. - /// Currently mods are only allowed to install files into the Mods folder. - /// The only exception to this rule is the Vanilla content package. - /// - /// - /// - public static bool IsModFilePathAllowed(string path) - { - if (GameMain.VanillaContent.Files.Any(f => string.Equals(System.IO.Path.GetFullPath(f.Path).CleanUpPath(), - System.IO.Path.GetFullPath(path).CleanUpPath(), - StringComparison.InvariantCultureIgnoreCase))) - { - //file is in vanilla package, this is allowed - return true; - } - - while (true) - { - string temp = Barotrauma.IO.Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(temp)) { break; } - path = temp; - } - return path == "Mods"; - } - - /// - /// Returns all xml files from all the loaded content packages. - /// - public static IEnumerable GetAllContentFiles(IEnumerable contentPackages) - { - return contentPackages.SelectMany(f => f.Files).Select(f => f.Path).Where(p => p.EndsWith(".xml")); - } - - public static IEnumerable GetFilesOfType(IEnumerable contentPackages, ContentType type) - { - return contentPackages.SelectMany(f => f.Files).Where(f => f.Type == type); - } - public static IEnumerable GetFilesOfType(IEnumerable contentPackages, params ContentType[] types) - { - return contentPackages.SelectMany(f => f.Files).Where(f => types.Contains(f.Type)); - } - - public IEnumerable GetFilesOfType(ContentType type) - { - return Files.Where(f => f.Type == type).Select(f => f.Path); - } - - public static void AddPackage(ContentPackage newPackage) - { - if (corePackages.Concat(regularPackages).Any(p => p.Name.Equals(newPackage.Name, StringComparison.OrdinalIgnoreCase))) - { - DebugConsole.ThrowError($"Attempted to add \"{newPackage.Name}\" more than once!\n{Environment.StackTrace}"); - } - if (newPackage.IsCorePackage) - { - corePackages.AddOnMainThread(newPackage); - } - else - { - regularPackages.AddOnMainThread(newPackage); - } - } - - public static void RemovePackage(ContentPackage package) - { - if (package.IsCorePackage) { corePackages.RemoveOnMainThread(package); } - else { regularPackages.RemoveOnMainThread(package); } - } - - public static void LoadAll() - { - string folder = Folder; - if (!Directory.Exists(folder)) - { - try - { - Directory.CreateDirectory(folder); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to create directory \"" + folder + "\"", e); - return; - } - } - - IEnumerable files = Directory.GetFiles(folder, "*.xml"); - - corePackages.ClearOnMainThread(); - var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList(); - regularPackages.ClearOnMainThread(); - - foreach (string filePath in files) - { - var newPackage = new ContentPackage(filePath); - if (!newPackage.IsCorrupt) { AddPackage(newPackage); } - } - - IEnumerable modDirectories = Directory.GetDirectories("Mods"); - foreach (string modDirectory in modDirectories) - { - if (Barotrauma.IO.Path.GetFileName(modDirectory.TrimEnd(Barotrauma.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } - string modFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); - string copyingFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); - if (File.Exists(copyingFilePath)) - { - //this mod didn't clean up its copying file; assume it's corrupted and delete it - Directory.Delete(modDirectory, true); - } - else if (File.Exists(modFilePath)) - { - var newPackage = new ContentPackage(modFilePath); - if (!newPackage.IsCorrupt) - { - AddPackage(newPackage); - } - } - } - SortContentPackages(p => prevRegularPackages.IndexOf(p.Name.ToLowerInvariant())); - GameMain.Config?.SortContentPackages(); - } - - public static void SortContentPackages(Func order, bool refreshAll = false, GameSettings config = null) - { - var ordered = regularPackages - .OrderBy(p => order(p)) - .ThenBy(p => regularPackages.IndexOf(p)) - .ToList(); - regularPackages.ClearOnMainThread(); regularPackages.AddRangeOnMainThread(ordered); - (config ?? GameMain.Config)?.SortContentPackages(refreshAll); - } - - public void Delete() - { - try - { - if (IsCorePackage) - { - corePackages.RemoveOnMainThread(this); - if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); } - } - else - { - regularPackages.RemoveOnMainThread(this); - if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); } - } - GameMain.Config.SaveNewPlayerConfig(); - File.Delete(Path); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to delete content package \"" + Name + "\".", e); - return; - } - } - } - - public class ContentFile - { - public string Path; - public ContentType Type; - - public ContentPackage ContentPackage; - - public ContentFile(string path, ContentType type, ContentPackage contentPackage = null) - { - Path = path.CleanUpPath(); - - Type = type; - ContentPackage = contentPackage; - } - - public override string ToString() - { - return Path; - } - } - -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index cd253c368..dfbf631f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -159,7 +159,7 @@ namespace Barotrauma return new string[][] { commands.SelectMany(c => c.names).ToArray(), - new string[0] + Array.Empty() }; })); @@ -169,7 +169,7 @@ namespace Barotrauma NewMessage("***************", Color.Cyan); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (string.IsNullOrEmpty(itemPrefab.Name)) continue; + if (itemPrefab.Name.IsNullOrEmpty()) { continue; } string text = $"- {itemPrefab.Name}"; if (itemPrefab.Tags.Any()) { @@ -194,20 +194,14 @@ namespace Barotrauma commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { - List characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).Select(f => f.Path).ToList(); - for (int i = 0; i < characterFiles.Count; i++) - { - characterFiles[i] = Path.GetFileNameWithoutExtension(characterFiles[i]).ToLowerInvariant(); - } - - foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) - { - characterFiles.Add(jobPrefab.Name); - } + string[] creatureAndJobNames = + CharacterPrefab.Prefabs.Select(p => p.Identifier.Value) + .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value)) + .ToArray(); return new string[][] { - characterFiles.ToArray(), + creatureAndJobNames.ToArray(), new string[] { "near", "inside", "outside", "cursor" } }; }, isCheat: true)); @@ -239,9 +233,9 @@ namespace Barotrauma List itemNames = new List(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (!itemNames.Contains(itemPrefab.Name)) + if (!itemNames.Contains(itemPrefab.Name.Value)) { - itemNames.Add(itemPrefab.Name); + itemNames.Add(itemPrefab.Name.Value); } } @@ -330,7 +324,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - PermissionPreset.List.Select(pp => pp.Name).ToArray() + PermissionPreset.List.Select(pp => pp.Name.Value).ToArray() }; })); @@ -521,7 +515,7 @@ namespace Barotrauma if (targetCharacter == null) { return; } targetCharacter.GodMode = !targetCharacter.GodMode; - NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on " + targetCharacter.Name), Color.White); + NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on ") + targetCharacter.Name, Color.White); }, () => { @@ -599,7 +593,7 @@ namespace Barotrauma } foreach (Character character in Character.CharacterList) { - if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName.Equals(args[0], StringComparison.OrdinalIgnoreCase)) + if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName == args[0]) { ThrowError(character.ID + ": " + character.Name.ToString()); } @@ -612,7 +606,7 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + a.Identifier == args[0]); if (afflictionPrefab == null) { ThrowError("Affliction \"" + args[0] + "\" not found."); @@ -650,7 +644,7 @@ namespace Barotrauma { return new string[][] { - AfflictionPrefab.List.Select(a => a.Name).ToArray(), + AfflictionPrefab.Prefabs.Select(a => a.Name.Value).ToArray(), new string[] { "1" }, Character.CharacterList.Select(c => c.Name).ToArray(), Enum.GetNames(typeof(LimbType)).ToArray() @@ -751,10 +745,10 @@ namespace Barotrauma commands.Add(new Command("triggerevent", "triggerevent [identifier]: Created a new event.", (string[] args) => { - List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList(); + List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList(); if (GameMain.GameSession?.EventManager != null && args.Length > 0) { - EventPrefab eventPrefab = eventPrefabs.Find(prefab => string.Equals(prefab.Identifier, args[0], StringComparison.InvariantCultureIgnoreCase)); + EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]); if (eventPrefab != null) { @@ -776,11 +770,11 @@ namespace Barotrauma NewMessage("Failed to trigger event", Color.Red); }, isCheat: true, getValidArgs: () => { - List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList(); + List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList(); return new[] { - eventPrefabs.Select(prefab => prefab.Identifier).Distinct().ToArray() + eventPrefabs.Select(prefab => prefab.Identifier).Distinct().Select(id => id.Value).ToArray() }; })); @@ -792,7 +786,7 @@ namespace Barotrauma return; } - string skillIdentifier = args[0]; + Identifier skillIdentifier = args[0].ToIdentifier(); string levelString = args[1]; Character character = args.Length >= 3 ? FindMatchingCharacter(args.Skip(2).ToArray(), false) : Character.Controlled; @@ -807,7 +801,7 @@ namespace Barotrauma if (float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out float level) || isMax) { if (isMax) { level = 100; } - if (skillIdentifier.Equals("all", StringComparison.OrdinalIgnoreCase)) + if (skillIdentifier == "all") { foreach (Skill skill in character.Info.Job.Skills) { @@ -829,7 +823,7 @@ namespace Barotrauma { return new[] { - Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier).ToArray() ?? new string[0], + Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty(), new[]{ "max" }, Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), }; @@ -848,7 +842,7 @@ namespace Barotrauma if (character != null) { TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => - c.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + c.Identifier == args[0] || c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { @@ -864,12 +858,12 @@ namespace Barotrauma List talentNames = new List(); foreach (TalentPrefab talent in TalentPrefab.TalentPrefabs) { - talentNames.Add(talent.DisplayName); + talentNames.Add(talent.DisplayName.Value); } return new string[][] { - talentNames.ToArray(), + talentNames.Select(id => id).ToArray(), Character.CharacterList.Select(c => c.Name).Distinct().ToArray() }; }, isCheat: true)); @@ -892,7 +886,7 @@ namespace Barotrauma ThrowError($"Failed to find the job \"{args[0]}\"."); return; } - if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) + if (!TalentTree.JobTalentTrees.TryGet(job.Identifier, out TalentTree talentTree)) { ThrowError($"No talents configured for the job \"{args[0]}\"."); return; @@ -918,7 +912,7 @@ namespace Barotrauma () => { List availableArgs = new List() { "All" }; - availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name)); + availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name.Value)); return new string[][] { availableArgs.ToArray(), @@ -979,7 +973,7 @@ namespace Barotrauma { NewMessage("Level seed: " + Level.Loaded.Seed); NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); - NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none") + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none")); + NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier())); NewMessage("Mirrored: " + Level.Loaded.Mirrored); NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y); NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); @@ -1066,7 +1060,7 @@ namespace Barotrauma Character character = FindMatchingCharacter(args, false); if (character == null) { return; } - Entity.Spawner?.AddToRemoveQueue(character); + Entity.Spawner?.AddEntityToRemoveQueue(character); }, () => { @@ -1096,17 +1090,17 @@ namespace Barotrauma IEnumerable TestLevels() { SubmarineInfo selectedSub = null; - string subName = GameMain.Config.QuickStartSubmarineName; - if (!string.IsNullOrEmpty(subName)) + Identifier subName = GameSettings.CurrentConfig.QuickStartSub; + if (subName != Identifier.Empty) { - selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(subName, StringComparison.OrdinalIgnoreCase)); + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); } int count = 0; while (true) { var gamesession = new GameSession( - SubmarineInfo.SavedSubmarines.GetRandom(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)), + SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)), GameModePreset.DevSandbox); string seed = ToolBox.RandomSeed(16); gamesession.StartRound(seed); @@ -1188,7 +1182,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)) is { } faction) + if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == args[0]) is { } faction) { if (float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { @@ -1210,7 +1204,7 @@ namespace Barotrauma } }, () => { - return new[] { FactionPrefab.Prefabs.Select(f => f.Identifier).ToArray() }; + return new[] { FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray() }; }, true)); commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => @@ -1266,7 +1260,7 @@ namespace Barotrauma return; } - var upgradePrefab = UpgradePrefab.Find(args[0]); + var upgradePrefab = UpgradePrefab.Find(args[0].ToIdentifier()); if (upgradePrefab == null) { @@ -1301,7 +1295,7 @@ namespace Barotrauma foreach (MapEntity targetItem in targetItems) { - Upgrade existingUpgrade = targetItem.GetUpgrade(args[0]); + Upgrade existingUpgrade = targetItem.GetUpgrade(args[0].ToIdentifier()); if (!(targetItem is ISerializableEntity sEntity)) { continue; } @@ -1333,7 +1327,7 @@ namespace Barotrauma { return new[] { - UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray() + UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray() }; }, true)); @@ -1362,11 +1356,11 @@ namespace Barotrauma foreach (UpgradeCategory category in UpgradeCategory.Categories) { - if (!string.IsNullOrWhiteSpace(categoryIdentifier) && !category.Identifier.Equals(categoryIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; } + if (!string.IsNullOrWhiteSpace(categoryIdentifier) && category.Identifier != categoryIdentifier) { continue; } foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) { if (!prefab.UpgradeCategories.Contains(category)) { continue; } - if (!string.IsNullOrWhiteSpace(prefabIdentifier) && !prefab.Identifier.Equals(prefabIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; } + if (!string.IsNullOrWhiteSpace(prefabIdentifier) && prefab.Identifier != prefabIdentifier) { continue; } int targetLevel = prefab.MaxLevel - upgradeManager.GetRealUpgradeLevel(prefab, category); for (int i = 0; i < targetLevel; i++) @@ -1382,8 +1376,8 @@ namespace Barotrauma { return new[] { - UpgradeCategory.Categories.Select(c => c.Identifier).Distinct().ToArray(), - UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray() + UpgradeCategory.Categories.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray(), + UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray() }; }, true)); @@ -1404,7 +1398,7 @@ namespace Barotrauma commands.Add(new Command("oxygen|air", "oxygen/air: Replenishes the oxygen levels in every room to 100%.", (string[] args) => { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.OxygenPercentage = 100.0f; } @@ -1432,7 +1426,7 @@ namespace Barotrauma c.SetAllDamage(200.0f, 0.0f, 0.0f); } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.BallastFlora?.Kill(); } @@ -1597,14 +1591,14 @@ namespace Barotrauma if (pumps.Any()) { - BallastFloraPrefab prefab = string.IsNullOrWhiteSpace(secondaryArgument) ? BallastFloraPrefab.Prefabs.First() : BallastFloraPrefab.Find(secondaryArgument); + BallastFloraPrefab prefab = string.IsNullOrWhiteSpace(secondaryArgument) ? BallastFloraPrefab.Prefabs.First() : BallastFloraPrefab.Find(secondaryArgument.ToIdentifier()); if (prefab == null) { ThrowError($"No such behavior: {secondaryArgument}"); return; } - Pump random = pumps.GetRandom(); + Pump random = pumps.GetRandomUnsynced(); random.InfectBallast(prefab.Identifier, allowMultiplePerShip: true); NewMessage($"Infected {random.Name} with {prefab.Identifier} in {random.Item.CurrentHull.DisplayName}.", Color.Green); return; @@ -1617,7 +1611,7 @@ namespace Barotrauma { if (int.TryParse(secondaryArgument, out int value)) { - foreach (Hull hull in Hull.hullList.Where(h => h.BallastFlora != null)) + foreach (Hull hull in Hull.HullList.Where(h => h.BallastFlora != null)) { BallastFloraBehavior bs = hull.BallastFlora; bs.GrowthWarps = value; @@ -1632,7 +1626,7 @@ namespace Barotrauma }, isCheat: true, getValidArgs: () => { string[] primaries = { "infect", "growthwarp" }; - string[] identifiers = BallastFloraPrefab.Prefabs.Select(bfp => bfp.Identifier).Distinct().ToArray(); + string[] identifiers = BallastFloraPrefab.Prefabs.Select(bfp => bfp.Identifier).Distinct().Select(i => i.Value).ToArray(); return new[] { primaries, identifiers }; })); @@ -1660,8 +1654,10 @@ namespace Barotrauma commands.Add(new Command("verboselogging", "verboselogging: Toggle verbose console logging on/off. When on, additional debug information is written to the debug console.", (string[] args) => { - GameSettings.VerboseLogging = !GameSettings.VerboseLogging; - NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White); + var config = GameSettings.CurrentConfig; + config.VerboseLogging = !GameSettings.CurrentConfig.VerboseLogging; + GameSettings.SetCurrentConfig(config); + NewMessage((GameSettings.CurrentConfig.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White); }, isCheat: false)); commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); })); @@ -1671,7 +1667,7 @@ namespace Barotrauma if (args.Length > 0) { string packageName = string.Join(" ", args); - var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase)); + var package = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase)); if (package == null) { ThrowError("Content package \"" + packageName + "\" not found."); @@ -1683,14 +1679,14 @@ namespace Barotrauma } else { - GameMain.Config.AllEnabledPackages.First().CalculateHash(logging: true); + ContentPackageManager.EnabledPackages.Core.CalculateHash(logging: true); } }, () => { return new string[][] { - GameMain.Config.AllEnabledPackages.Select(cp => cp.Name).ToArray() + ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name).ToArray() }; })); @@ -1776,9 +1772,9 @@ namespace Barotrauma msg += "\nBalance: " + location.StoreCurrentBalance; msg += "\nPrice modifier: " + location.StorePriceModifier + "%"; msg += "\nDaily specials:"; - location.DailySpecials.ForEach(i => msg += "\n - " + i.Name); + location.DailySpecials.ForEach(i => msg += "\n - " + i.Name.Value); msg += "\nRequested goods:"; - location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name); + location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name.Value); NewMessage(msg); } else @@ -2051,7 +2047,7 @@ namespace Barotrauma switch (args[1].ToLowerInvariant()) { case "inside": - spawnPoint = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); + spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub); break; case "outside": spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); @@ -2098,7 +2094,7 @@ namespace Barotrauma } catch { - DebugConsole.ThrowError($"\"{args[2]}\" is not a valid team id."); + ThrowError($"\"{args[2]}\" is not a valid team id."); } } @@ -2106,8 +2102,8 @@ namespace Barotrauma if (human) { - var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.Server) : 0; - CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant); + var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0; + CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant); spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8)); if (GameMain.GameSession != null) { @@ -2121,7 +2117,7 @@ namespace Barotrauma } else { - if (CharacterPrefab.FindBySpeciesName(args[0]) != null) + if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) != null) { Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8)); } @@ -2143,7 +2139,7 @@ namespace Barotrauma if (itemPrefab == null) { errorMsg = "Item \"" + itemNameOrId + "\" not found!"; - var matching = ItemPrefab.Prefabs.Find(me => me.Name.ToLowerInvariant().StartsWith(itemNameOrId) && me is ItemPrefab); + var matching = ItemPrefab.Prefabs.Find(me => me.Name.StartsWith(itemNameOrId, StringComparison.OrdinalIgnoreCase) && me is ItemPrefab); if (matching != null) { errorMsg += $" Did you mean \"{matching.Name}\"?"; @@ -2204,7 +2200,7 @@ namespace Barotrauma } else { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnPos.Value); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value); } } else if (spawnInventory != null) @@ -2217,7 +2213,7 @@ namespace Barotrauma } else { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); } static void onItemSpawned(Item item) @@ -2246,6 +2242,9 @@ namespace Barotrauma NewMessage(command, color.Value, isCommand: true, isError: false); } + public static void NewMessage(LocalizedString msg, Color? color = null, bool debugOnly = false) + => NewMessage(msg.Value, color, debugOnly); + public static void NewMessage(string msg, Color? color = null, bool debugOnly = false) { color ??= Color.White; @@ -2284,7 +2283,7 @@ namespace Barotrauma #if CLIENT activeQuestionText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, 0), listBox.Content.RectTransform), - " >>" + question, font: GUI.SmallFont, wrap: true) + " >>" + question, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = Color.Cyan @@ -2352,14 +2351,21 @@ namespace Barotrauma public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); + public static void Log(LocalizedString message) => Log(message?.Value); + public static void Log(string message) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { NewMessage(message, Color.Gray); } } + public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) + { + ThrowError(error.Value); + } + public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) { if (e != null) @@ -2383,7 +2389,7 @@ namespace Barotrauma { error += "\n" + Environment.StackTrace.CleanupStackTrace(); } - System.Diagnostics.Debug.WriteLine(error); + System.Diagnostics.Debug.WriteLine($"ThrowError: {error}"); #if CLIENT if (createMessageBox) @@ -2408,11 +2414,6 @@ namespace Barotrauma #if CLIENT private static IEnumerable CreateMessageBox(string errorMsg) { - while (GUI.Style == null) - { - yield return null; - } - new GUIMessageBox(TextManager.Get("Error"), errorMsg); yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs index 2d7792808..2b0cd4756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs @@ -1,102 +1,92 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { - class DecalManager + public class GrimeSprite : Prefab { - public PrefabCollection Prefabs { get; private set; } - - public readonly List GrimeSprites = new List(); - private Dictionary> grimeSpritesByFile = new Dictionary>(); - - public DecalManager() + public GrimeSprite(Sprite spr, DecalsFile file, int indexInFile) : base(file, $"{nameof(GrimeSprite)}{indexInFile}".ToIdentifier()) { - Prefabs = new PrefabCollection(); - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals)) - { - LoadFromFile(configFile); - } + Sprite = spr; + IndexInFile = indexInFile; } - public void LoadFromFile(ContentFile configFile) + public readonly int IndexInFile; + + public Sprite Sprite { get; private set; } + + public override void Dispose() + { + Sprite?.Remove(); Sprite = null; + } + } + + static class DecalManager + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public static int GrimeSpriteCount { get; private set; } = 0; + + public static readonly PrefabCollection GrimeSprites = new PrefabCollection( + onAdd: (sprite, b) => GrimeSpriteCount = Math.Max(GrimeSpriteCount, sprite.IndexInFile+1), + onRemove: (s) => + GrimeSpriteCount = GrimeSprites.AllPrefabs + .SelectMany(kvp => kvp.Value) + .Where(p => p != s).Select(p => p.IndexInFile+1).MaxOrNull() ?? 0, + onSort: null, onAddOverrideFile: null, onRemoveOverrideFile: null); + + public static void LoadFromFile(DecalsFile configFile) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { return; } - if (grimeSpritesByFile.ContainsKey(configFile.Path)) - { - foreach (Sprite sprite in grimeSpritesByFile[configFile.Path]) - { - sprite.Remove(); - GrimeSprites.Remove(sprite); - } - grimeSpritesByFile.Remove(configFile.Path); - } - bool allowOverriding = false; - var mainElement = doc.Root; + var mainElement = doc.Root.FromPackage(configFile.ContentPackage); if (doc.Root.IsOverride()) { - mainElement = doc.Root.FirstElement(); + mainElement = mainElement.FirstElement(); allowOverriding = true; } - foreach (XElement sourceElement in mainElement.Elements()) + int grimeIndex = 0; + foreach (var sourceElement in mainElement.Elements()) { var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; + bool isOverride = allowOverriding || sourceElement.IsOverride(); string name = element.Name.ToString().ToLowerInvariant(); switch (name) { case "grime": - if (!grimeSpritesByFile.ContainsKey(configFile.Path)) - { - grimeSpritesByFile.Add(configFile.Path, new List()); - } - var grimeSprite = new Sprite(element); - GrimeSprites.Add(grimeSprite); - grimeSpritesByFile[configFile.Path].Add(grimeSprite); + GrimeSprites.Add(new GrimeSprite(new Sprite(element), configFile, grimeIndex), isOverride); + grimeIndex++; break; default: - if (Prefabs.ContainsKey(name)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate decal prefab '{name}' found in '{configFile.Path}'! Each decal prefab must have a unique name. " + - "Use tags to override prefabs."); - continue; - } - } - var newPrefab = new DecalPrefab(element, configFile); - Prefabs.Add(newPrefab, allowOverriding || sourceElement.IsOverride()); - newPrefab.CalculatePrefabUIntIdentifier(Prefabs); + var prefab = new DecalPrefab(element, configFile); + Prefabs.Add(prefab, isOverride); break; } } } - public void RemoveByFile(string filePath) + public static void RemoveByFile(DecalsFile configFile) { - Prefabs.RemoveByFile(filePath); - if (grimeSpritesByFile.ContainsKey(filePath)) - { - foreach (Sprite sprite in grimeSpritesByFile[filePath]) - { - sprite.Remove(); - GrimeSprites.Remove(sprite); - } - grimeSpritesByFile.Remove(filePath); - } + Prefabs.RemoveByFile(configFile); + GrimeSprites.RemoveByFile(configFile); } - public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null) + public static void SortAll() + { + Prefabs.SortAll(); + GrimeSprites.SortAll(); + } + + public static Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null) { string lowerCaseDecalName = decalName.ToLowerInvariant(); if (!Prefabs.ContainsKey(lowerCaseDecalName)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs index bc0551501..8d8a0d6b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs @@ -5,29 +5,11 @@ using System.Xml.Linq; namespace Barotrauma { - class DecalPrefab : IPrefab, IHasUintIdentifier, IDisposable + class DecalPrefab : PrefabWithUintIdentifier { - public readonly string Name; + public string Name => Identifier.Value; - public string OriginalName { get { return Name; } } - - public string Identifier - { - get; - private set; - } - - /// - /// Unique identifier that's generated by hashing the prefab's string identifier. - /// Used to reduce the amount of bytes needed to write decal data into network messages in multiplayer. - /// - public uint UIntIdentifier { get; set; } - - public string FilePath { get; private set; } - - public ContentPackage ContentPackage { get; private set; } - - public void Dispose() + public override void Dispose() { foreach (Sprite spr in Sprites) { @@ -44,19 +26,11 @@ namespace Barotrauma public readonly float FadeOutTime; public readonly float FadeInTime; - public DecalPrefab(XElement element, ContentFile file) + public DecalPrefab(ContentXElement element, DecalsFile file) : base(file, new Identifier(element.Name.LocalName)) { - Name = element.Name.ToString(); - - Identifier = Name.ToLowerInvariant(); - - FilePath = file.Path; - - ContentPackage = file.ContentPackage; - Sprites = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index af68ef218..3feeabf1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -26,7 +26,7 @@ namespace Barotrauma public override string ToString() { - return "ArtifactEvent (" + (itemPrefab == null ? "null" : itemPrefab.Name) + ")"; + return $"ArtifactEvent ({(itemPrefab == null ? "null" : itemPrefab.Name)})"; } public ArtifactEvent(EventPrefab prefab) @@ -56,7 +56,7 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { spawnPos = Level.Loaded.GetRandomItemPos( - (Rand.Value(Rand.RandSync.Server) < 0.5f) ? + (Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ? Level.PositionType.MainPath | Level.PositionType.SidePath : Level.PositionType.Cave | Level.PositionType.Ruin, 500.0f, 10000.0f, 30.0f, SpawnPosFilter); @@ -79,7 +79,7 @@ namespace Barotrauma if (itemContainer.Combine(item, user: null)) break; // Placement successful } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Initialized ArtifactEvent (" + item.Name + ")", Color.White); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs index 6e770f730..454867de5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs @@ -7,19 +7,19 @@ namespace Barotrauma { class AfflictionAction : EventAction { - [Serialize("", true)] - public string Affliction { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Affliction { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Strength { get; set; } - [Serialize(LimbType.None, true)] + [Serialize(LimbType.None, IsPropertySaveable.Yes)] public LimbType LimbType { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public AfflictionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; @@ -36,7 +36,7 @@ namespace Barotrauma public override void Update(float deltaTime) { if (isFinished) { return; } - var afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(p => p.Identifier.Equals(Affliction, StringComparison.InvariantCultureIgnoreCase)); + var afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(p => p.Identifier == Affliction); if (afflictionPrefab != null) { var targets = ParentEvent.GetTargets(TargetTag); @@ -44,14 +44,28 @@ namespace Barotrauma { if (target != null && target is Character character) { - var limb = LimbType != LimbType.None ? character.AnimController.GetLimb(LimbType) : null; - if (Strength > 0.0f) + if (LimbType != LimbType.None) { - character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength)); + var limb = character.AnimController.GetLimb(LimbType); + if (Strength > 0.0f) + { + character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength)); + } + else if (Strength < 0.0f) + { + character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -Strength); + } } - else if (Strength < 0.0f) + else { - character.CharacterHealth.ReduceAffliction(limb, Affliction, -Strength); + if (Strength > 0.0f) + { + character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength)); + } + else if (Strength < 0.0f) + { + character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -Strength); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs index 5eb38c547..05f27d592 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs @@ -11,9 +11,9 @@ namespace Barotrauma public SubactionGroup Failure = null; protected bool? succeeded = null; - public BinaryOptionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public BinaryOptionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - foreach (XElement elem in element.Elements()) + foreach (var elem in element.Elements()) { string elemName = elem.Name.LocalName; if (elemName.Equals("success", StringComparison.InvariantCultureIgnoreCase)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index b5ff3c16a..1aca440b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -8,23 +8,23 @@ namespace Barotrauma { internal class CheckAfflictionAction : BinaryOptionAction { - [Serialize("", true)] - public string Identifier { get; set; } = ""; + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", true)] - public string TargetTag { get; set; } = ""; + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } = Identifier.Empty; - [Serialize(LimbType.None, true, "Only check afflictions on the specified limb type")] + [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")] public LimbType TargetLimb { get; set; } - [Serialize(true, true, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] + [Serialize(true, IsPropertySaveable.Yes, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] public bool AllowLimbAfflictions { get; set; } - public CheckAfflictionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public CheckAfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { - if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(TargetTag)) { return false; } + if (Identifier.IsEmpty || TargetTag.IsEmpty) { return false; } List targets = ParentEvent.GetTargets(TargetTag).OfType().ToList(); foreach (var target in targets) @@ -42,7 +42,7 @@ namespace Barotrauma return limbType == TargetLimb || true; }); - if (afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } + if (afflictions.Any(a => a.Identifier == Identifier)) { return true; } } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index fd60cddb2..92ff33999 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -1,21 +1,22 @@ #nullable enable using System; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { class CheckDataAction : BinaryOptionAction { - [Serialize("", true)] - public string Identifier { get; set; } = null!; + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", true)] - public string Condition { get; set; } = null!; + [Serialize("", IsPropertySaveable.Yes)] + public string Condition { get; set; } = ""; - [Serialize(false, true, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")] + [Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")] public bool ForceString { get; set; } - [Serialize(false, true, "Performs the comparison against a metadata by identifier instead of a constant value")] + [Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value")] public bool CheckAgainstMetadata { get; set; } protected object? value2; @@ -23,11 +24,11 @@ namespace Barotrauma protected PropertyConditional.OperatorType Operator { get; set; } - public CheckDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public CheckDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (string.IsNullOrEmpty(Condition)) { - Condition = element.GetAttributeString("value", string.Empty); + Condition = element.GetAttributeString("value", string.Empty)!; if (string.IsNullOrEmpty(Condition)) { DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element})."); @@ -43,10 +44,8 @@ namespace Barotrauma string value = Condition; if (splitString.Length > 0) { - for (int i = 1; i < splitString.Length; i++) - { - value = splitString[i] + (i > 1 && i < splitString.Length ? " " : ""); - } + #warning Is this correct? + value = string.Join(" ", splitString.Skip(1)); } else { @@ -61,7 +60,7 @@ namespace Barotrauma if (CheckAgainstMetadata) { object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier); - object? metadata2 = campaignMode.CampaignMetadata.GetValue(value); + object? metadata2 = campaignMode.CampaignMetadata.GetValue(value.ToIdentifier()); if (metadata1 == null || metadata2 == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index b1ba89a2a..b7874d26a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -6,22 +6,22 @@ namespace Barotrauma { class CheckItemAction : BinaryOptionAction { - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string ItemIdentifiers { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string ItemTags { get; set; } - private readonly string[] itemIdentifierSplit; - private readonly string[] itemTags; + private readonly Identifier[] itemIdentifierSplit; + private readonly Identifier[] itemTags; - public CheckItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public CheckItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - itemIdentifierSplit = ItemIdentifiers.Split(','); - itemTags = ItemTags.Split(","); + itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers(); + itemTags = ItemTags.Split(",").ToIdentifiers(); } protected override bool? DetermineSuccess() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs index f19e759a9..8d3141adf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs @@ -4,10 +4,10 @@ namespace Barotrauma { class CheckMoneyAction : BinaryOptionAction { - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } - public CheckMoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs index b34f80de9..5f6f19e47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs @@ -7,10 +7,10 @@ namespace Barotrauma { class CheckReputationAction : CheckDataAction { - [Serialize(ReputationAction.ReputationType.None, true)] + [Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes)] public ReputationAction.ReputationType TargetType { get; set; } - public CheckReputationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public CheckReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override float GetFloat(CampaignMode campaignMode) { @@ -18,7 +18,7 @@ namespace Barotrauma { case ReputationAction.ReputationType.Faction: { - Faction? faction = campaignMode.Factions.Find(f => f.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); + Faction? faction = campaignMode.Factions.Find(f => f.Prefab.Identifier == Identifier); if (faction != null) { return faction.Reputation.Value; } break; } @@ -54,7 +54,7 @@ namespace Barotrauma } return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckReputationAction)} -> (Type: {TargetType.ColorizeObject()}, " + - $"{(string.IsNullOrWhiteSpace(Identifier) ? string.Empty : $"Identifier: {Identifier.ColorizeObject()}, ")}" + + $"{(Identifier.IsEmpty ? string.Empty : $"Identifier: {Identifier.ColorizeObject()}, ")}" + $"Success: {succeeded.ColorizeObject()}, Expression: {condition})"; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs index 73263a685..4de2f7c4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs @@ -4,12 +4,12 @@ namespace Barotrauma { class ClearTagAction : EventAction { - [Serialize("", true)] - public string Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Tag { get; set; } private bool isFinished; - public ClearTagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public ClearTagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goToLabel) => isFinished; @@ -22,7 +22,7 @@ namespace Barotrauma { if (isFinished) { return; } - if (!string.IsNullOrWhiteSpace(Tag)) + if (!Tag.IsEmpty) { ParentEvent.RemoveTag(Tag); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs index c42a96330..26320d4f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs @@ -7,25 +7,25 @@ namespace Barotrauma { class CombatAction : EventAction { - [Serialize(AIObjectiveCombat.CombatMode.Offensive, true)] + [Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes)] public AIObjectiveCombat.CombatMode CombatMode { get; set; } - [Serialize(false, true, description: "Did this NPC start the fight (as an aggressor)?")] + [Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)?")] public bool IsInstigator { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, true)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] public AIObjectiveCombat.CombatMode GuardReaction { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, true)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] public AIObjectiveCombat.CombatMode WitnessReaction { get; set; } - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize("", true)] - public string EnemyTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier EnemyTag { get; set; } - [Serialize(120.0f, true)] + [Serialize(120.0f, IsPropertySaveable.Yes)] public float CoolDown { get; set; } private bool isFinished = false; @@ -33,7 +33,7 @@ namespace Barotrauma private IEnumerable affectedNpcs = null; - public CombatAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public CombatAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override void Update(float deltaTime) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 9f53430a9..00c439419 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -26,37 +26,37 @@ namespace Barotrauma /// const float BlockOtherConversationsDuration = 5.0f; - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Text { get; set; } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int DefaultOption { get; set; } - [Serialize("", true)] - public string SpeakerTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SpeakerTag { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool WaitForInteraction { get; set; } - [Serialize("", true, "Tag to assign to whoever invokes the conversation")] - public string InvokerTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation")] + public Identifier InvokerTag { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool FadeToBlack { get; set; } - [Serialize(true, true, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")] + [Serialize(true, IsPropertySaveable.Yes, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")] public bool EndEventIfInterrupted { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string EventSprite { get; set; } - [Serialize(DialogTypes.Regular, true)] + [Serialize(DialogTypes.Regular, IsPropertySaveable.Yes)] public DialogTypes DialogType { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } private Character speaker; @@ -79,12 +79,12 @@ namespace Barotrauma private bool interrupt; - public ConversationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { actionCount++; Identifier = actionCount; Options = new List(); - foreach (XElement elem in element.Elements()) + foreach (var elem in element.Elements()) { if (elem.Name.LocalName.Equals("option", StringComparison.InvariantCultureIgnoreCase)) { @@ -230,7 +230,7 @@ namespace Barotrauma return; } - if (!string.IsNullOrEmpty(SpeakerTag)) + if (!SpeakerTag.IsEmpty) { if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; @@ -254,7 +254,7 @@ namespace Barotrauma #if CLIENT speaker.SetCustomInteract( TryStartConversation, - TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameMain.Config.KeyBindText(InputType.Use))); + TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else speaker.SetCustomInteract( TryStartConversation, @@ -286,7 +286,7 @@ namespace Barotrauma private bool ShouldInterrupt() { IEnumerable targets = Enumerable.Empty(); - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); if (!targets.Any()) { return true; } @@ -294,7 +294,7 @@ namespace Barotrauma if (speaker != null) { - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } } @@ -324,7 +324,7 @@ namespace Barotrauma private void TryStartConversation(Character speaker, Character targetCharacter = null) { IEnumerable targets = Enumerable.Empty(); - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); if (!targets.Any() || IsBlockedByAnotherConversation(targets)) { return; } @@ -335,8 +335,7 @@ namespace Barotrauma prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); prevGotoObjective = humanAI.ObjectiveManager.GetObjective(); humanAI.SetForcedOrder( - Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)), - option: string.Empty, orderGiver: null); + new Order(OrderPrefab.Prefabs["wait"], Barotrauma.Identifier.Empty, null, orderGiver: null)); if (targets.Any()) { Entity closestTarget = null; @@ -357,7 +356,7 @@ namespace Barotrauma } } - if (targetCharacter != null && !string.IsNullOrWhiteSpace(InvokerTag)) + if (targetCharacter != null && !InvokerTag.IsEmpty) { ParentEvent.AddTarget(InvokerTag, targetCharacter); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index 083e54b3a..edd2baefa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -28,12 +28,12 @@ namespace Barotrauma } } - public SubactionGroup(ScriptedEvent scriptedEvent, XElement elem) + public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem) { Text = elem.Attribute("text")?.Value ?? ""; Actions = new List(); EndConversation = elem.GetAttributeBool("endconversation", false); - foreach (XElement e in elem.Elements()) + foreach (var e in elem.Elements()) { if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { @@ -100,7 +100,7 @@ namespace Barotrauma public readonly ScriptedEvent ParentEvent; - public EventAction(ScriptedEvent parentEvent, XElement element) + public EventAction(ScriptedEvent parentEvent, ContentXElement element) { ParentEvent = parentEvent; SerializableProperty.DeserializeProperties(this, element); @@ -132,7 +132,7 @@ namespace Barotrauma public virtual void Update(float deltaTime) { } - public static EventAction Instantiate(ScriptedEvent scriptedEvent, XElement element) + public static EventAction Instantiate(ScriptedEvent scriptedEvent, ContentXElement element) { Type actionType = null; try @@ -146,7 +146,7 @@ namespace Barotrauma return null; } - ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(XElement) }); + ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(ContentXElement) }); try { return constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs index 8ef70a6a6..0adf73e69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs @@ -8,13 +8,13 @@ namespace Barotrauma { class FireAction : EventAction { - [Serialize(10.0f, true)] + [Serialize(10.0f, IsPropertySaveable.Yes)] public float Size { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public FireAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public FireAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 2da290284..12290178a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -6,18 +6,18 @@ namespace Barotrauma { class GiveSkillExpAction : EventAction { - [Serialize("", true)] - public string Skill { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Skill { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Amount { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public GiveSkillExpAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public GiveSkillExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrEmpty(TargetTag)) + if (TargetTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": GiveSkillExpAction without a target tag (the action needs to know whose skill to check)."); } @@ -40,7 +40,7 @@ namespace Barotrauma var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); foreach (var target in targets) { - target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount); + target.Info?.IncreaseSkillLevel(Skill, Amount); } isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index 44d1895dc..6f186b127 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -4,10 +4,10 @@ namespace Barotrauma { class GoTo : EventAction { - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Name { get; set; } - public GoTo(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs index aecef1c64..47ff662f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs @@ -7,13 +7,13 @@ namespace Barotrauma { class GodModeAction : EventAction { - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool Enabled { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public GodModeAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public GodModeAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs index 3c81f85d3..a95a7dea7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs @@ -4,10 +4,10 @@ namespace Barotrauma { class Label : EventAction { - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Name { get; set; } - public Label(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public Label(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 301ca0f74..da7661209 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -8,33 +8,33 @@ namespace Barotrauma { class MissionAction : EventAction { - [Serialize("", true)] - public string MissionIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } - [Serialize("", true)] - public string MissionTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionTag { get; set; } - [Serialize("", true, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")] + [Serialize("", IsPropertySaveable.Yes, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")] public string LocationType { get; set; } - [Serialize(0, true, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] + [Serialize(0, IsPropertySaveable.Yes, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] public int MinLocationDistance { get; set; } - [Serialize(true, true, description: "If true, the mission has to be unlocked in a location further on the campaign map.")] + [Serialize(true, IsPropertySaveable.Yes, description: "If true, the mission has to be unlocked in a location further on the campaign map.")] public bool UnlockFurtherOnMap { get; set; } - [Serialize(false, true, description: "If true, a suitable location is forced on the map if one isn't found.")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, a suitable location is forced on the map if one isn't found.")] public bool CreateLocationIfNotFound { get; set; } private bool isFinished; - public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrEmpty(MissionIdentifier) && string.IsNullOrEmpty(MissionTag)) + if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured."); } - if (!string.IsNullOrEmpty(MissionIdentifier) && !string.IsNullOrEmpty(MissionTag)) + if (!MissionIdentifier.IsEmpty && !MissionTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); } @@ -63,18 +63,18 @@ namespace Barotrauma var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet()); if (emptyLocation != null) { - emptyLocation.ChangeType(Barotrauma.LocationType.List.Find(lt => lt.Identifier.Equals(LocationType, StringComparison.OrdinalIgnoreCase))); + emptyLocation.ChangeType(Barotrauma.LocationType.Prefabs[LocationType]); unlockLocation = emptyLocation; } } if (unlockLocation != null) { - if (!string.IsNullOrEmpty(MissionIdentifier)) + if (!MissionIdentifier.IsEmpty) { prefab = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); } - else if (!string.IsNullOrEmpty(MissionTag)) + else if (!MissionTag.IsEmpty) { prefab = unlockLocation.UnlockMissionByTag(MissionTag); } @@ -87,7 +87,7 @@ namespace Barotrauma DebugConsole.NewMessage($"Unlocked mission \"{prefab.Name}\" in the location \"{unlockLocation.Name}\"."); #if CLIENT new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), - new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; @@ -118,7 +118,7 @@ namespace Barotrauma private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet checkedLocations) { var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (currLocation.Type.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase) && currDistance >= MinLocationDistance && + if (currLocation.Type.Identifier == locationType && currDistance >= MinLocationDistance && (!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X)) { return currLocation; @@ -136,7 +136,7 @@ namespace Barotrauma public override string ToDebugString() { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(string.IsNullOrEmpty(MissionIdentifier) ? MissionTag : MissionIdentifier)})"; + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})"; } #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index 5dfe8e9f0..ae7273383 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -5,9 +5,9 @@ namespace Barotrauma { class MoneyAction : EventAction { - public MoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public MoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } private bool isFinished; @@ -28,7 +28,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign) { campaign.Money += Amount; - GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value); #if SERVER (campaign as MultiPlayerCampaign).LastUpdateID++; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index b7c31e334..d4a5f8ed7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -7,18 +7,18 @@ namespace Barotrauma { class NPCChangeTeamAction : EventAction { - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize(CharacterTeamType.None, true)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)] public CharacterTeamType TeamTag { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool AddToCrew { get; set; } private bool isFinished = false; - public NPCChangeTeamAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private List affectedNpcs = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index eaa494848..7cc2f28ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -8,18 +8,18 @@ namespace Barotrauma { class NPCFollowAction : EventAction { - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool Follow { get; set; } private bool isFinished = false; - public NPCFollowAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public NPCFollowAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private List affectedNpcs = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 751731fbc..aa2bee231 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -6,16 +6,16 @@ namespace Barotrauma { class NPCWaitAction : EventAction { - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool Wait { get; set; } private bool isFinished = false; - public NPCWaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public NPCWaitAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private IEnumerable affectedNpcs; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index f08ec8423..3ab365409 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -6,10 +6,10 @@ namespace Barotrauma { class RNGAction : BinaryOptionAction { - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Chance { get; set; } - public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public RNGAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (Chance >= 1.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index 2d1e1e4d7..d39a5e666 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -7,20 +7,20 @@ namespace Barotrauma { class RemoveItemAction : EventAction { - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize("", true)] - public string ItemIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } - [Serialize(1, true)] + [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } - public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrWhiteSpace(ItemIdentifier)) + if (ItemIdentifier.IsEmpty) { - ItemIdentifier = element.GetAttributeString("itemidentifiers", null) ?? element.GetAttributeString("identifier", ""); + ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty)); } } @@ -62,17 +62,17 @@ namespace Barotrauma var item = inventory.FindItem(it => it != null && !removedItems.Contains(it) && - (string.IsNullOrEmpty(ItemIdentifier) || it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)), recursive: true); + (ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true); if (item == null) { break; } - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); } } else if (target is Item item) { - if (string.IsNullOrEmpty(ItemIdentifier) || item.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)) + if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier) { - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); if (removedItems.Count >= Amount) { break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index d4f86a1f0..14be7bdba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -15,15 +15,15 @@ namespace Barotrauma Faction } - public ReputationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public ReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Increase { get; set; } - [Serialize("", true)] - public string Identifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } - [Serialize(ReputationType.None, true)] + [Serialize(ReputationType.None, IsPropertySaveable.Yes)] public ReputationType TargetType { get; set; } private bool isFinished; @@ -47,7 +47,7 @@ namespace Barotrauma { case ReputationType.Faction: { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); if (faction != null) { faction.Reputation.AddReputation(Increase); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs index d9e180e3a..7e33a2373 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs @@ -12,16 +12,16 @@ namespace Barotrauma Add } - public SetDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public SetDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(OperationType.Set, true)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes)] public OperationType Operation { get; set; } - [Serialize(null, true)] + [Serialize(null, IsPropertySaveable.Yes)] public string Value { get; set; } = null!; - [Serialize("", true)] - public string Identifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } private bool isFinished; @@ -47,7 +47,7 @@ namespace Barotrauma isFinished = true; } - public static void PerformOperation(CampaignMetadata metadata, string identifier, object value, OperationType operation) + public static void PerformOperation(CampaignMetadata metadata, Identifier identifier, object value, OperationType operation) { if (metadata == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs index 6c0ac0e1f..bdded765e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs @@ -19,16 +19,16 @@ namespace Barotrauma Mechanical } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Multiplier { get; set; } - [Serialize(OperationType.Set, true)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes)] public OperationType Operation { get; set; } - [Serialize(PriceMultiplierType.Store, true)] + [Serialize(PriceMultiplierType.Store, IsPropertySaveable.Yes)] public PriceMultiplierType TargetMultiplier { get; set; } - public SetPriceMultiplierAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public SetPriceMultiplierAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index d63601890..c74ddea95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -7,21 +7,21 @@ namespace Barotrauma { class SkillCheckAction : BinaryOptionAction { - [Serialize("", true)] - public string RequiredSkill { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier RequiredSkill { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float RequiredLevel { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool ProbabilityBased { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public SkillCheckAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public SkillCheckAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrEmpty(TargetTag)) + if (TargetTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": SkillCheckAction without a target tag (the action needs to know whose skill to check)."); } @@ -33,11 +33,11 @@ namespace Barotrauma if (ProbabilityBased) { - return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) / RequiredLevel > Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); + return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill) / RequiredLevel > Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); } else { - return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) >= RequiredLevel); + return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill) >= RequiredLevel); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 8293cc1e5..a05b0a183 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -19,39 +19,39 @@ namespace Barotrauma BeaconStation } - [Serialize("", true, description: "Species name of the character to spawn.")] - public string SpeciesName { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Species name of the character to spawn.")] + public Identifier SpeciesName { get; set; } - [Serialize("", true, description: "Identifier of the NPC set to choose from.")] - public string NPCSetIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the NPC set to choose from.")] + public Identifier NPCSetIdentifier { get; set; } - [Serialize("", true, description: "Identifier of the NPC.")] - public string NPCIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the NPC.")] + public Identifier NPCIdentifier { get; set; } - [Serialize(true, true, description: "Should taking the items of this npc be considered as stealing?")] + [Serialize(true, IsPropertySaveable.Yes, description: "Should taking the items of this npc be considered as stealing?")] public bool LootingIsStealing { get; set; } - [Serialize("", true, description: "Identifier of the item to spawn.")] - public string ItemIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item to spawn.")] + public Identifier ItemIdentifier { get; set; } - [Serialize("", true, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")] + public Identifier TargetTag { get; set; } - [Serialize("", true, description: "Tag of an entity with an inventory to spawn the item into.")] - public string TargetInventory { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")] + public Identifier TargetInventory { get; set; } - [Serialize(SpawnLocationType.MainSub, true)] + [Serialize(SpawnLocationType.MainSub, IsPropertySaveable.Yes)] public SpawnLocationType SpawnLocation { get; set; } - [Serialize(SpawnType.Human, true)] + [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] public SpawnType SpawnPointType { get; set; } - [Serialize("", true)] - public string SpawnPointTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SpawnPointTag { get; set; } - private readonly HashSet targetModuleTags = new HashSet(); + private readonly HashSet targetModuleTags = new HashSet(); - [Serialize("", true, "What outpost module tags does the entity prefer to spawn in.")] + [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] public string TargetModuleTags { get => string.Join(",", targetModuleTags); @@ -63,13 +63,13 @@ namespace Barotrauma string[] splitTags = value.Split(','); foreach (var s in splitTags) { - targetModuleTags.Add(s); + targetModuleTags.Add(s.ToIdentifier()); } } } } - [Serialize(false, true, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")] public bool IgnoreByAI { get; set; } private bool spawned; @@ -77,7 +77,7 @@ namespace Barotrauma private readonly bool ignoreSpawnPointType; - public SpawnAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase)); } @@ -104,16 +104,16 @@ namespace Barotrauma { if (spawned) { return; } - if (!string.IsNullOrEmpty(NPCSetIdentifier) && !string.IsNullOrEmpty(NPCIdentifier)) + if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty) { HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); if (humanPrefab != null) { ISpatialEntity spawnPos = GetSpawnPos(); - Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { if (newCharacter == null) { return; } - newCharacter.Prefab = humanPrefab; + newCharacter.HumanPrefab = humanPrefab; newCharacter.TeamID = CharacterTeamType.FriendlyNPC; newCharacter.EnableDespawn = false; humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); @@ -126,7 +126,7 @@ namespace Barotrauma } } humanPrefab.InitializeCharacter(newCharacter, spawnPos); - if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) + if (!TargetTag.IsEmpty && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); } @@ -134,18 +134,18 @@ namespace Barotrauma }); } } - else if (!string.IsNullOrEmpty(SpeciesName)) + else if (!SpeciesName.IsEmpty) { - Entity.Spawner.AddToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => { - if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) + if (!TargetTag.IsEmpty && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); } spawnedEntity = newCharacter; }); } - else if (!string.IsNullOrEmpty(ItemIdentifier)) + else if (!ItemIdentifier.IsEmpty) { if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab)) { @@ -154,7 +154,7 @@ namespace Barotrauma else { Inventory spawnInventory = null; - if (!string.IsNullOrEmpty(TargetInventory)) + if (!TargetInventory.IsEmpty) { var targets = ParentEvent.GetTargets(TargetInventory); if (targets.Any()) @@ -178,17 +178,17 @@ namespace Barotrauma if (spawnInventory == null) { - Entity.Spawner.AddToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned); } else { - Entity.Spawner.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); } void onSpawned(Item newItem) { if (newItem != null) { - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { ParentEvent.AddTarget(TargetTag, newItem); } @@ -221,7 +221,7 @@ namespace Barotrauma private ISpatialEntity GetSpawnPos() { - if (!string.IsNullOrWhiteSpace(SpawnPointTag)) + if (!SpawnPointTag.IsEmpty) { List potentialItems = SpawnLocation switch { @@ -234,10 +234,10 @@ namespace Barotrauma _ => throw new NotImplementedException() }; - var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandom(); + var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced(); if (item != null) { return item; } - var target = ParentEvent.GetTargets(SpawnPointTag).GetRandom(); + var target = ParentEvent.GetTargets(SpawnPointTag).GetRandomUnsynced(); if (target != null) { return target; } } @@ -247,7 +247,7 @@ namespace Barotrauma return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable()); } - public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false) + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false) { List potentialSpawnPoints = spawnLocation switch { @@ -301,7 +301,7 @@ namespace Barotrauma } //don't spawn in an airlock module if there are other options - var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false); + var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false); if (airlockSpawnPoints.Count() < validSpawnPoints.Count()) { validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints); @@ -310,7 +310,7 @@ namespace Barotrauma if (!validSpawnPoints.Any()) { DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})"); - return potentialSpawnPoints.GetRandom(); + return potentialSpawnPoints.GetRandomUnsynced(); } //avoid using waypoints if there's any actual spawnpoints available @@ -346,7 +346,7 @@ namespace Barotrauma } else { - return validSpawnPoints.GetRandom(); + return validSpawnPoints.GetRandomUnsynced(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index 8a3630247..4dc8d6adc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -9,19 +9,19 @@ namespace Barotrauma private int actionIndex; - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public StatusEffectAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public StatusEffectAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { actionIndex = 0; - foreach (XElement subElement in parentEvent.Prefab.ConfigElement.Descendants()) + foreach (var subElement in parentEvent.Prefab.ConfigElement.Descendants()) { if (subElement == element) { break; } actionIndex++; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index a1a054a1c..377fa8eb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -1,5 +1,9 @@ using Barotrauma.Extensions; using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -8,21 +12,35 @@ namespace Barotrauma { public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Criteria { get; set; } - [Serialize("", true)] - public string Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Tag { get; set; } - [Serialize(SubType.Any, true)] + [Serialize(SubType.Any, IsPropertySaveable.Yes)] public SubType SubmarineType { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool IgnoreIncapacitatedCharacters { get; set; } private bool isFinished = false; - public TagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + Taggers = new (string k, Action v)[] + { + ("players", v => TagPlayers()), + ("player", v => TagPlayers()), + ("bot", v => TagBots(playerCrewOnly: false)), + ("crew", v => TagCrew()), + ("humanprefabidentifier", TagHumansByIdentifier), + ("structureidentifier", TagStructuresByIdentifier), + ("itemidentifier", TagItemsByIdentifier), + ("itemtag", TagItemsByTag), + ("hullname", TagHullsByName) + }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); + } public override bool IsFinished(ref string goTo) { @@ -67,34 +85,34 @@ namespace Barotrauma #endif } - private void TagHumansByIdentifier(string identifier) + private void TagHumansByIdentifier(Identifier identifier) { foreach (Character c in Character.CharacterList) { - if (c.Prefab?.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase) ?? false) + if (c.HumanPrefab?.Identifier == identifier) { ParentEvent.AddTarget(Tag, c); } } } - private void TagStructuresByIdentifier(string identifier) + private void TagStructuresByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); } - private void TagItemsByIdentifier(string identifier) + private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier); } - private void TagItemsByTag(string tag) + private void TagItemsByTag(Identifier tag) { ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); } - private void TagHullsByName(string name) + private void TagHullsByName(Identifier name) { - ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name, StringComparison.OrdinalIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private bool SubmarineTypeMatches(Submarine sub) @@ -117,6 +135,8 @@ namespace Barotrauma } } + private readonly ImmutableDictionary> Taggers; + public override void Update(float deltaTime) { if (isFinished) { return; } @@ -126,32 +146,17 @@ namespace Barotrauma foreach (string entry in criteriaSplit) { string[] kvp = entry.Split(':'); - switch (kvp[0].Trim().ToLowerInvariant()) + Identifier key = kvp[0].Trim().ToIdentifier(); + Identifier value = kvp.Length > 1 ? kvp[1].Trim().ToIdentifier() : Identifier.Empty; + if (Taggers.TryGetValue(key, out Action tagger)) { - case "player": - TagPlayers(); - break; - case "bot": - TagBots(playerCrewOnly: false); - break; - case "crew": - TagCrew(); - break; - case "humanprefabidentifier": - if (kvp.Length > 1) { TagHumansByIdentifier(kvp[1].Trim()); } - break; - case "structureidentifier": - if (kvp.Length > 1) { TagStructuresByIdentifier(kvp[1].Trim()); } - break; - case "itemidentifier": - if (kvp.Length > 1) { TagItemsByIdentifier(kvp[1].Trim()); } - break; - case "itemtag": - if (kvp.Length > 1) { TagItemsByTag(kvp[1].Trim()); } - break; - case "hullname": - if (kvp.Length > 1) { TagHullsByName(kvp[1].Trim()); } - break; + tagger(value); + } + else + { + string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; + DebugConsole.ThrowError(errorMessage); + GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index c75d1ea35..470d6a129 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -8,42 +8,39 @@ namespace Barotrauma { class TriggerAction : EventAction { - [Serialize("", true, description: "Tag of the first entity that will be used for trigger checks.")] - public string Target1Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")] + public Identifier Target1Tag { get; set; } - [Serialize("", true, description: "Tag of the second entity that will be used for trigger checks.")] - public string Target2Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the second entity that will be used for trigger checks.")] + public Identifier Target2Tag { get; set; } - [Serialize("", true, description: "If set, the first target has to be within an outpost module of this type.")] - public string TargetModuleType { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "If set, the first target has to be within an outpost module of this type.")] + public Identifier TargetModuleType { get; set; } - [Serialize("", true, description: "Tag to apply to the first entity when the trigger check succeeds.")] - public string ApplyToTarget1 { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first entity when the trigger check succeeds.")] + public Identifier ApplyToTarget1 { get; set; } - [Serialize("", true, description: "Tag to apply to the second entity when the trigger check succeeds.")] - public string ApplyToTarget2 { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")] + public Identifier ApplyToTarget2 { get; set; } - [Serialize(0.0f, true, description: "Range both entities must be within to activate the trigger.")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range both entities must be within to activate the trigger.")] public float Radius { get; set; } - [Serialize(true, true, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] + [Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] public bool DisableInCombat { get; set; } - [Serialize(true, true, description: "If true, dead/unconscious characters cannot trigger the action.")] + [Serialize(true, IsPropertySaveable.Yes, description: "If true, dead/unconscious characters cannot trigger the action.")] public bool DisableIfTargetIncapacitated { get; set; } - [Serialize(false, true, description: "If true, one target must interact with the other to trigger the action.")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, one target must interact with the other to trigger the action.")] public bool WaitForInteraction { get; set; } - [Serialize(false, true, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] public bool AllowMultipleTargets { get; set; } private float distance; - - public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) - { - TargetModuleType = TargetModuleType?.ToLowerInvariant(); - } + + public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; public override bool IsFinished(ref string goTo) @@ -74,7 +71,7 @@ namespace Barotrauma { if (DisableInCombat && IsInCombat(e1)) { continue; } if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; } - if (!string.IsNullOrEmpty(TargetModuleType)) + if (!TargetModuleType.IsEmpty) { if (IsCloseEnoughToHull(e1, out Hull hull)) { @@ -143,7 +140,7 @@ namespace Barotrauma #if CLIENT npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, - TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameMain.Config.KeyBindText(InputType.Use))); + TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, @@ -226,7 +223,7 @@ namespace Barotrauma } else { - foreach (Hull potentialHull in Hull.hullList) + foreach (Hull potentialHull in Hull.HullList) { if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; } @@ -270,11 +267,11 @@ namespace Barotrauma private void Trigger(Entity entity1, Entity entity2) { ResetTargetIcons(); - if (!string.IsNullOrEmpty(ApplyToTarget1)) + if (!ApplyToTarget1.IsEmpty) { ParentEvent.AddTarget(ApplyToTarget1, entity1); } - if (!string.IsNullOrEmpty(ApplyToTarget2)) + if (!ApplyToTarget2.IsEmpty) { ParentEvent.AddTarget(ApplyToTarget2, entity2); } @@ -285,7 +282,7 @@ namespace Barotrauma public override string ToDebugString() { - if (string.IsNullOrEmpty(TargetModuleType)) + if (TargetModuleType.IsEmpty) { return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index 9a6aabf58..c7253cc72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -4,12 +4,12 @@ namespace Barotrauma { class TriggerEventAction : EventAction { - [Serialize("", true)] - public string Identifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } private bool isFinished; - public TriggerEventAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs index 51ef8e0cf..ac2b2fe9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class UnlockPathAction : EventAction { - public UnlockPathAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; @@ -36,7 +36,7 @@ namespace Barotrauma NotifyUnlock(connection); #else new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"), - new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); + Array.Empty(), type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs index a413f9668..6380f8f0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs @@ -5,12 +5,12 @@ namespace Barotrauma { class WaitAction : EventAction { - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Time { get; set; } private float timeRemaining; - public WaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public WaitAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { timeRemaining = Time; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 039131b10..d979fc275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -144,17 +144,17 @@ namespace Barotrauma seed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - seed ^= ToolBox.StringToInt(previousEvent.Identifier); + seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier); } } MTRandom rand = new MTRandom(seed); - EventSet initialEventSet = SelectRandomEvents(EventSet.List, requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; - initialEventSet = SelectRandomEvents(EventSet.List.FindAll(e => !e.Additive), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + initialEventSet = SelectRandomEvents(EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); } if (initialEventSet != null) { @@ -172,14 +172,14 @@ namespace Barotrauma //if the outpost is connected to a locked connection, create an event to unlock it if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { - var unlockPathPrefabs = EventSet.PrefabList.FindAll(e => e.UnlockPathEvent); - var unlockPathPrefabsForBiome = unlockPathPrefabs.FindAll(e => - string.IsNullOrEmpty(e.BiomeIdentifier) || - e.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase)); + var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent); + var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e => + e.BiomeIdentifier.IsEmpty || + e.BiomeIdentifier == level.LevelData.Biome.Identifier); var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? - ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, unlockPathPrefabsForBiome.Select(b => b.Commonness).ToList(), rand) : - ToolBox.SelectWeightedRandom(unlockPathPrefabs, unlockPathPrefabs.Select(b => b.Commonness).ToList(), rand); + ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) : + ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand); if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); @@ -204,7 +204,7 @@ namespace Barotrauma if (eventSet == null) { return; } if (eventSet.OncePerOutpost) { - foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.Prefabs)) + foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { if (!level.LevelData.NonRepeatableEvents.Contains(ep)) { @@ -237,16 +237,17 @@ namespace Barotrauma private void SelectSettings() { - if (EventManagerSettings.List.Count == 0) + if (!EventManagerSettings.Prefabs.Any()) { throw new InvalidOperationException("Could not select EventManager settings (no settings loaded)."); } + var orderedByDifficulty = EventManagerSettings.OrderedByDifficulty.ToArray(); if (level == null) { #if CLIENT if (GameMain.GameSession.GameMode is TestGameMode) { - settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)]; + settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient); if (settings != null) { eventThreshold = settings.DefaultEventThreshold; @@ -257,18 +258,18 @@ namespace Barotrauma throw new InvalidOperationException("Could not select EventManager settings (level not set)."); } - var suitableSettings = EventManagerSettings.List.FindAll(s => + var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s => level.Difficulty >= s.MinLevelDifficulty && - level.Difficulty <= s.MaxLevelDifficulty); + level.Difficulty <= s.MaxLevelDifficulty).ToArray(); - if (suitableSettings.Count == 0) + if (suitableSettings.Length == 0) { DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")"); - settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)]; + settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient); } else { - settings = suitableSettings[Rand.Int(suitableSettings.Count, Rand.RandSync.Server)]; + settings = suitableSettings.GetRandom(Rand.RandSync.ServerAndClient); } if (settings != null) { @@ -292,17 +293,17 @@ namespace Barotrauma public void PreloadContent(IEnumerable contentFiles) { - var filesToPreload = new List(contentFiles); + var filesToPreload = contentFiles.ToList(); foreach (Submarine sub in Submarine.Loaded) { if (sub.WreckAI == null) { continue; } - if (!string.IsNullOrEmpty(sub.WreckAI.Config.DefensiveAgent)) + if (!sub.WreckAI.Config.DefensiveAgent.IsEmpty) { var prefab = CharacterPrefab.FindBySpeciesName(sub.WreckAI.Config.DefensiveAgent); if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) { - filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + filesToPreload.Add(prefab.ContentFile); } } foreach (Item item in Item.ItemList) @@ -318,9 +319,9 @@ namespace Barotrauma foreach (var spawnInfo in statusEffect.SpawnCharacters) { var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName); - if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) + if (prefab != null && !filesToPreload.Contains(prefab.ContentFile)) { - filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + filesToPreload.Add(prefab.ContentFile); } } } @@ -331,77 +332,7 @@ namespace Barotrauma foreach (ContentFile file in filesToPreload) { - switch (file.Type) - { - case ContentType.Character: -#if CLIENT - CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(file.Path); - if (characterPrefab?.XDocument == null) - { - throw new Exception($"Failed to load the character config file from {file.Path}!"); - } - var doc = characterPrefab.XDocument; - var rootElement = doc.Root; - var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; - mainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e)); - if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string speciesName)) { continue; } - bool humanoid = mainElement.GetAttributeBool("humanoid", false); - CharacterPrefab originalCharacter; - if (characterPrefab.VariantOf != null) - { - originalCharacter = CharacterPrefab.FindBySpeciesName(characterPrefab.VariantOf); - var originalRoot = originalCharacter.XDocument.Root; - var originalMainElement = originalRoot.IsOverride() ? originalRoot.FirstElement() : originalRoot; - originalMainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e)); - if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string name)) { continue; } - speciesName = name; - if (mainElement.Attribute("humanoid") == null) - { - humanoid = originalMainElement.GetAttributeBool("humanoid", false); - } - } - RagdollParams ragdollParams; - try - { - if (humanoid) - { - ragdollParams = RagdollParams.GetRagdollParams(characterPrefab.VariantOf ?? speciesName); - } - else - { - ragdollParams = RagdollParams.GetRagdollParams(characterPrefab.VariantOf ?? speciesName); - } - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e); - continue; - } - - if (ragdollParams != null) - { - HashSet texturePaths = new HashSet - { - ragdollParams.Texture - }; - foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs) - { - if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); } - if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); } - if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); } - foreach (var decorativeSprite in limb.decorativeSpriteParams) - { - if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); } - } - } - foreach (string texturePath in texturePaths) - { - preloadedSprites.Add(new Sprite(texturePath, Vector2.Zero)); - } - } -#endif - break; - } + file.Preload(preloadedSprites.Add); } } @@ -435,7 +366,8 @@ namespace Barotrauma { if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } - DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue, debugOnly: true); + DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); + int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) @@ -464,31 +396,29 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab p) - => string.IsNullOrEmpty(p.BiomeIdentifier) || - p.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase); - - var suitablePrefabSubsets = eventSet.EventPrefabs - .FindAll(p => p.Prefabs.Any(isPrefabSuitable)); + bool isPrefabSuitable(EventPrefab e) + => e.BiomeIdentifier.IsEmpty || + e.BiomeIdentifier == level.LevelData?.Biome?.Identifier; + + var suitablePrefabSubsets = eventSet.EventPrefabs.Where( + e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); for (int i = 0; i < applyCount; i++) { if (eventSet.ChooseRandom) { - if (suitablePrefabSubsets.Count > 0) + if (suitablePrefabSubsets.Any()) { var unusedEvents = suitablePrefabSubsets.ToList(); for (int j = 0; j < eventSet.EventCount; j++) { - if (unusedEvents.All(e => e.Prefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } - EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Prefabs.Max(p => CalculateCommonness(p, e.Commonness))).ToList(), rand); + if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } + EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand); (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; if (eventPrefabs != null && rand.NextDouble() <= probability) { - var finalPrefabs = eventPrefabs.Where(isPrefabSuitable).ToArray(); - var finalPrefabCommonnesses = finalPrefabs.Select(p => p.Commonness).ToArray(); - var eventPrefab = ToolBox.SelectWeightedRandom(finalPrefabs, finalPrefabCommonnesses, rand); - + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); @@ -503,7 +433,7 @@ namespace Barotrauma } } } - if (eventSet.ChildSets.Count > 0) + if (eventSet.ChildSets.Any()) { var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); if (newEventSet != null) @@ -517,10 +447,8 @@ namespace Barotrauma foreach ((IEnumerable eventPrefabs, float commonness, float probability) in suitablePrefabSubsets) { if (rand.NextDouble() > probability) { continue; } - - var finalPrefabs = eventPrefabs.Where(isPrefabSuitable).ToArray(); - var finalPrefabCommonnesses = finalPrefabs.Select(p => p.Commonness).ToArray(); - var eventPrefab = ToolBox.SelectWeightedRandom(finalPrefabs, finalPrefabCommonnesses, rand); + + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); @@ -540,7 +468,7 @@ namespace Barotrauma } } - private EventSet SelectRandomEvents(List eventSets, bool? requireCampaignSet = null, Random random = null) + private EventSet SelectRandomEvents(IReadOnlyList eventSets, bool? requireCampaignSet = null, Random random = null) { if (level == null) { return null; } Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); @@ -549,7 +477,7 @@ namespace Barotrauma eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType && - (string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase))); + (es.BiomeIdentifier.IsEmpty || es.BiomeIdentifier == level.LevelData.Biome.Identifier)); if (requireCampaignSet.HasValue) { @@ -579,7 +507,7 @@ namespace Barotrauma { allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || - set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase))); + set.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier)); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -786,7 +714,7 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } + if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup(CharacterPrefab.HumanSpeciesName)) { continue; } if (!(character.AIController is EnemyAIController enemyAI)) { continue; } @@ -836,7 +764,7 @@ namespace Barotrauma float holeCount = 0.0f; float waterAmount = 0.0f; float dryHullVolume = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; } if (GameMain.GameSession?.GameMode is PvPMode) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs index 3572f831d..7d5fece59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs @@ -6,12 +6,25 @@ using Microsoft.Xna.Framework; namespace Barotrauma { - class EventManagerSettings + class EventManagerSettings : PrefabWithUintIdentifier { - public static readonly List List = new List(); + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public static IOrderedEnumerable OrderedByDifficulty + { + get + { + return Prefabs.OrderBy(p => (p.MinLevelDifficulty + p.MaxLevelDifficulty) * 0.5f) + .ThenBy(p => p.UintIdentifier); + } + } - public readonly string Identifier; - public readonly string Name; + public static EventManagerSettings GetByDifficultyPercentile(float p) + { + EventManagerSettings[] settings = OrderedByDifficulty.ToArray(); + return settings[Math.Clamp((int)(settings.Length * p), 0, settings.Length - 1)]; + } + + public readonly LocalizedString Name; //How much the event threshold increases per second. 0.0005f = 0.03f per minute public readonly float EventThresholdIncrease = 0.0005f; @@ -26,61 +39,19 @@ namespace Barotrauma public readonly float FreezeDurationWhenCrewAway = 60.0f * 10.0f; - public static void Init() + public override void Dispose() { } + + public EventManagerSettings(XElement element, EventManagerSettingsFile file) : base(file, element.NameAsIdentifier()) { - List.Clear(); - foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings)) - { - Load(file); - } - } + Name = TextManager.Get("difficulty." + Identifier).Fallback(Identifier.Value); + EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", EventThresholdIncrease); + DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", DefaultEventThreshold); + EventCooldown = element.GetAttributeFloat("EventCooldown", EventCooldown); - private static void Load(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var mainElement = doc.Root; - bool allowOverriding = false; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - allowOverriding = true; - } - foreach (XElement subElement in mainElement.Elements()) - { - var element = subElement.IsOverride() ? subElement.FirstElement() : subElement; - string identifier = element.Name.ToString(); - var duplicate = List.FirstOrDefault(e => e.Identifier.ToString().Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (duplicate != null) - { - if (allowOverriding || subElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file.Path}'", Color.Yellow); - List.Remove(duplicate); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': Another element with the name '{identifier}' found! Each element must have a unique name. Use tags if you want to override an existing preset."); - continue; - } - } - List.Add(new EventManagerSettings(element)); - } - List.Sort((x, y) => { return Math.Sign((x.MinLevelDifficulty + x.MaxLevelDifficulty) / 2.0f - (y.MinLevelDifficulty + y.MaxLevelDifficulty) / 2.0f); }); - } + MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", MinLevelDifficulty); + MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", MaxLevelDifficulty); - public EventManagerSettings(XElement element) - { - Identifier = element.Name.ToString(); - Name = TextManager.Get("difficulty." + Identifier, returnNull: true) ?? Identifier; - EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", 0.0005f); - DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f); - EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f); - - MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", 0.0f); - MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", 100.0f); - - FreezeDurationWhenCrewAway = element.GetAttributeFloat("FreezeDurationWhenCrewAway", 10.0f * 60.0f); + FreezeDurationWhenCrewAway = element.GetAttributeFloat("FreezeDurationWhenCrewAway", FreezeDurationWhenCrewAway); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 856f99d79..db5fe38b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -4,23 +4,25 @@ using System.Xml.Linq; namespace Barotrauma { - class EventPrefab + class EventPrefab : Prefab { - public readonly XElement ConfigElement; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public readonly ContentXElement ConfigElement; public readonly Type EventType; public readonly float Probability; public readonly bool TriggerEventCooldown; - public float Commonness; - public string Identifier; - public string BiomeIdentifier; - public float SpawnDistance; + public readonly float Commonness; + public readonly Identifier BiomeIdentifier; + public readonly float SpawnDistance; - public bool UnlockPathEvent; - public string UnlockPathTooltip; - public int UnlockPathReputation; - public string UnlockPathFaction; + public readonly bool UnlockPathEvent; + public readonly string UnlockPathTooltip; + public readonly int UnlockPathReputation; + public readonly string UnlockPathFaction; - public EventPrefab(XElement element) + public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) + : base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier)) { ConfigElement = element; @@ -37,8 +39,7 @@ namespace Barotrauma DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); } - Identifier = ConfigElement.GetAttributeString("identifier", string.Empty); - BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty); + BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); @@ -73,6 +74,8 @@ namespace Barotrauma return instance; } + public override void Dispose() { } + public override string ToString() { return $"EventPrefab ({Identifier})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 227528cee..2d91d363c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -7,13 +7,29 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma -{ - class EventSet +{ +#if CLIENT + class EventSprite : Prefab + { + public readonly static PrefabCollection Prefabs = new PrefabCollection(); + + public readonly Sprite Sprite; + + public EventSprite(ContentXElement element, RandomEventsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) + { + Sprite = new Sprite(element); + } + + public override void Dispose() { Sprite?.Remove(); } + } +#endif + + class EventSet : Prefab { internal class EventDebugStats { public readonly EventSet RootSet; - public readonly Dictionary MonsterCounts = new Dictionary(); + public readonly Dictionary MonsterCounts = new Dictionary(); public float MonsterStrength; public EventDebugStats(EventSet rootSet) @@ -22,13 +38,7 @@ namespace Barotrauma } } - public static List List - { - get; - private set; - } - - public static readonly List PrefabList = new List(); + public readonly static PrefabCollection Prefabs = new PrefabCollection(); #if CLIENT private static readonly Dictionary EventSprites = new Dictionary(); @@ -47,21 +57,23 @@ namespace Barotrauma public static List GetAllEventPrefabs() { - List eventPrefabs = new List(PrefabList); - foreach (var eventSet in List) + List eventPrefabs = EventPrefab.Prefabs.ToList(); + foreach (var eventSet in Prefabs) { - eventPrefabs.AddRange(eventSet.EventPrefabs.SelectMany(ep => ep.Prefabs)); - foreach (var childSet in eventSet.ChildSets) - { - eventPrefabs.AddRange(childSet.EventPrefabs.SelectMany(ep => ep.Prefabs)); - } + AddSetEventPrefabsToList(eventPrefabs, eventSet); } return eventPrefabs; } - public static EventPrefab GetEventPrefab(string identifer) + public static void AddSetEventPrefabsToList(List list, EventSet set) { - return GetAllEventPrefabs().Find(prefab => string.Equals(prefab.Identifier, identifer, StringComparison.Ordinal)); + list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs)); + foreach (var childSet in set.ChildSets) { AddSetEventPrefabsToList(list, childSet); } + } + + public static EventPrefab GetEventPrefab(Identifier identifier) + { + return GetAllEventPrefabs().Find(prefab => prefab.Identifier == identifier); } public readonly bool IsCampaignSet; @@ -69,11 +81,11 @@ namespace Barotrauma //0-100 public readonly float MinLevelDifficulty, MaxLevelDifficulty; - public readonly string BiomeIdentifier; + public readonly Identifier BiomeIdentifier; public readonly LevelData.LevelType LevelType; - public readonly string[] LocationTypeIdentifiers; + public readonly ImmutableArray LocationTypeIdentifiers; public readonly bool ChooseRandom; @@ -99,68 +111,100 @@ namespace Barotrauma public readonly bool TriggerEventCooldown; public readonly bool Additive; + + public readonly float DefaultCommonness; + public readonly ImmutableDictionary OverrideCommonness; - public readonly Dictionary Commonness; - - public struct SubEventPrefab + public readonly struct SubEventPrefab { - public SubEventPrefab(string debugIdentifier, string[] prefabIdentifiers, float? commonness, float? probability) + public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) { - EventPrefab tryFindPrefab(string id) + PrefabOrIdentifier = prefabOrIdentifiers; + SelfCommonness = commonness; + SelfProbability = probability; + } + + public readonly Either PrefabOrIdentifier; + public IEnumerable EventPrefabs + { + get { - var prefab = PrefabList.Find(p => p.Identifier.Equals(id, StringComparison.OrdinalIgnoreCase)); - if (prefab is null) + if (PrefabOrIdentifier.TryGet(out EventPrefab p)) { - DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\" - could not find the event prefab \"{id}\"."); + yield return p; + } + else + { + foreach (var id in (Identifier[])PrefabOrIdentifier) + { + yield return EventPrefab.Prefabs[id]; + } } - return prefab; } - - this.Prefabs = prefabIdentifiers - .Select(tryFindPrefab) - .Where(p => p != null) - .ToImmutableArray(); - this.Commonness = commonness ?? this.Prefabs.Select(p => p.Commonness).MaxOrNull() ?? 0.0f; - this.Probability = probability ?? this.Prefabs.Select(p => p.Probability).MaxOrNull() ?? 0.0f; } + public readonly float? SelfCommonness; + public float Commonness => SelfCommonness ?? EventPrefabs.MaxOrNull(p => p.Commonness) ?? 0.0f; - public SubEventPrefab(EventPrefab prefab, float commonness, float probability) + public readonly float? SelfProbability; + public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f; + + public void Deconstruct(out IEnumerable eventPrefabs, out float commonness, out float probability) { - Prefabs = prefab.ToEnumerable().ToImmutableArray(); - Commonness = commonness; - Probability = probability; - } - - public readonly ImmutableArray Prefabs; - public readonly float Commonness; - public readonly float Probability; - - public void Deconstruct(out IEnumerable prefabs, out float commonness, out float probability) - { - prefabs = Prefabs; + eventPrefabs = EventPrefabs; commonness = Commonness; probability = Probability; } } - - public readonly List EventPrefabs; + public readonly ImmutableArray EventPrefabs; - public readonly List ChildSets; + public readonly ImmutableArray ChildSets; - public string DebugIdentifier + private static Identifier DetermineIdentifier(EventSet parent, XElement element, RandomEventsFile file) { - get; - private set; - } = ""; + Identifier retVal = element.GetAttributeIdentifier("identifier", Identifier.Empty); - private EventSet(XElement element, string debugIdentifier, EventSet parentSet = null) + if (retVal.IsEmpty) + { + if (parent is null) + { + if (file.ContentPackage is CorePackage) + { + throw new Exception($"Error in {file.Path}: All root EventSets in a core package must have identifiers"); + } + else + { + DebugConsole.AddWarning($"{file.Path}: All root EventSets should have an identifier"); + } + } + + XElement currElement = element; + string siblingIndices = ""; + while (currElement.Parent != null) + { + int siblingIndex = currElement.ElementsBeforeSelf().Count(); + siblingIndices = $"-{siblingIndex}{siblingIndices}"; + if (parent != null) { break; } + currElement = currElement.Parent; + } + + retVal = + ((parent != null + ? parent.Identifier.Value + : $"{file.ContentPackage.Name}-{file.Path}") + + siblingIndices) + .ToIdentifier(); + } + return retVal; + } + + public EventSet(ContentXElement element, RandomEventsFile file, EventSet parentSet = null) + : base(file, DetermineIdentifier(parentSet, element, file)) { - DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier; - Commonness = new Dictionary(); - EventPrefabs = new List(); - ChildSets = new List(); + var eventPrefabs = new List(); + var childSets = new List(); + var overrideCommonness = new Dictionary(); - BiomeIdentifier = element.GetAttributeString("biome", string.Empty); + BiomeIdentifier = element.GetAttributeIdentifier("biome", Barotrauma.Identifier.Empty); MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0); MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty); @@ -169,14 +213,14 @@ namespace Barotrauma string levelTypeStr = element.GetAttributeString("leveltype", "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) { - DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\". \"{levelTypeStr}\" is not a valid level type."); + DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); } - string[] locationTypeStr = element.GetAttributeStringArray("locationtype", null); + Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null); if (locationTypeStr != null) { - LocationTypeIdentifiers = locationTypeStr; - if (LocationType.List.Any()) { CheckLocationTypeErrors(); } + LocationTypeIdentifiers = locationTypeStr.ToImmutableArray(); + //if (LocationType.List.Any()) { CheckLocationTypeErrors(); } //TODO: perform validation elsewhere } MinIntensity = element.GetAttributeFloat("minintensity", 0.0f); @@ -198,164 +242,77 @@ namespace Barotrauma TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); - Commonness[""] = element.GetAttributeFloat("commonness", 1.0f); - foreach (XElement subElement in element.Elements()) + DefaultCommonness = 1.0f; + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "commonness": - Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f); + DefaultCommonness = subElement.GetAttributeFloat("commonness", 0.0f); foreach (XElement overrideElement in subElement.Elements()) { - if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) + if (overrideElement.NameAsIdentifier() == "override") { - string levelType = overrideElement.GetAttributeString("leveltype", "").ToLowerInvariant(); - if (!Commonness.ContainsKey(levelType)) + Identifier levelType = overrideElement.GetAttributeIdentifier("leveltype", ""); + if (!overrideCommonness.ContainsKey(levelType)) { - Commonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f)); + overrideCommonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f)); } } } break; case "eventset": - ChildSets.Add(new EventSet(subElement, this.DebugIdentifier + "-" + ChildSets.Count, this)); + childSets.Add(new EventSet(subElement, file, this)); break; default: //an element with just an identifier = reference to an event prefab if (!subElement.HasElements && subElement.Attributes().First().Name.ToString().Equals("identifier", StringComparison.OrdinalIgnoreCase)) { - string[] identifiers = subElement.GetAttributeStringArray("identifier", Array.Empty()); - + Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); float commonness = subElement.GetAttributeFloat("commonness", -1f); float probability = subElement.GetAttributeFloat("probability", -1f); - EventPrefabs.Add(new SubEventPrefab( - debugIdentifier, + eventPrefabs.Add(new SubEventPrefab( identifiers, commonness >= 0f ? commonness : (float?)null, probability >= 0f ? probability : (float?)null)); } else { - var prefab = new EventPrefab(subElement); - EventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); + var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier()); + eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); } break; } } + + EventPrefabs = eventPrefabs.ToImmutableArray(); + ChildSets = childSets.ToImmutableArray(); + OverrideCommonness = overrideCommonness.ToImmutableDictionary(); } public void CheckLocationTypeErrors() { if (LocationTypeIdentifiers == null) { return; } - foreach (string locationTypeId in LocationTypeIdentifiers) + foreach (Identifier locationTypeId in LocationTypeIdentifiers) { - if (!LocationType.List.Any(lt => lt.Identifier.Equals(locationTypeId, StringComparison.OrdinalIgnoreCase))) + if (!LocationType.Prefabs.ContainsKey(locationTypeId)) { - DebugConsole.ThrowError($"Error in event set \"{DebugIdentifier}\". Location type \"{locationTypeId}\" not found."); + DebugConsole.ThrowError($"Error in event set \"{Identifier}\". Location type \"{locationTypeId}\" not found."); } } } public float GetCommonness(Level level) { - string key = level.GenerationParams?.Identifier ?? ""; - return Commonness.ContainsKey(key) ? Commonness[key] : Commonness[""]; - } - - public static void LoadPrefabs() - { -#if CLIENT - EventSprites.ForEach(pair => pair.Value?.Remove()); - EventSprites.Clear(); -#endif - List = new List(); - var configFiles = GameMain.Instance.GetFilesOfType(ContentType.RandomEvents); - - if (!configFiles.Any()) - { - DebugConsole.ThrowError("No config files for random events found in the selected content package"); - return; - } - - List configElements = new List(); - Dictionary filePaths = new Dictionary(); - - foreach (ContentFile configFile in configFiles) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { continue; } - - var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - if (doc.Root.IsOverride()) - { - DebugConsole.NewMessage($"Overriding all random events using the file {configFile.Path}", Color.Yellow); - List.Clear(); - } - - foreach (XElement element in doc.Root.Elements()) - { - configElements.Add(element); - filePaths[element] = configFile.Path; - } - } - - //load event prefabs first so we can link to them when loading event sets - foreach (XElement element in configElements) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "eventprefabs": - foreach (var subElement in element.Elements()) - { - // Warn if an event prefab has no identifier as this would make it impossible to refer to - if (!element.GetAttributeBool("suppresswarnings", false) && string.IsNullOrWhiteSpace(subElement.GetAttributeString("identifier", string.Empty))) - { - DebugConsole.AddWarning($"An event prefab {subElement.Name} in {filePaths[element]} is missing an identifier."); - } - - PrefabList.Add(new EventPrefab(subElement)); - } - break; - case "eventsprites": -#if CLIENT - foreach (var subElement in element.Elements()) - { - string identifier = subElement.GetAttributeString("identifier", string.Empty); - - if (EventSprites.ContainsKey(identifier)) - { - EventSprites[identifier]?.Remove(); - EventSprites[identifier] = new Sprite(subElement); - continue; - } - else - { - EventSprites.Add(identifier, new Sprite(subElement)); - } - } -#endif - break; - } - } - - int i = 0; - foreach (XElement element in configElements) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "eventset": - List.Add(new EventSet(element, i.ToString())); - i++; - break; - } - } + Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty; + return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness; } public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) { List debugLines = new List(); - foreach (var eventSet in List) + foreach (var eventSet in Prefabs) { List stats = new List(); for (int i = 0; i < simulatedRoundCount; i++) @@ -364,7 +321,7 @@ namespace Barotrauma CheckEventSet(newStats, eventSet, filter); stats.Add(newStats); } - debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): "); + debugLines.Add($"Event stats ({eventSet.Identifier}): "); LogEventStats(stats, debugLines, fullLog); } @@ -379,16 +336,18 @@ namespace Barotrauma { for (int i = 0; i < thisSet.EventCount; i++) { - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList()); - if (eventPrefab.Prefabs.Any(p => p != null)) + var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced); + if (eventPrefab.EventPrefabs.Any(p => p != null)) { - AddEvents(stats, eventPrefab.Prefabs, filter); + AddEvents(stats, eventPrefab.EventPrefabs, filter); unusedEvents.Remove(eventPrefab); } } } - List values = thisSet.ChildSets.SelectMany(s => s.Commonness.Values).ToList(); - EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values); + List values = thisSet.ChildSets + .SelectMany(s => s.DefaultCommonness.ToEnumerable().Concat(s.OverrideCommonness.Values)) + .ToList(); + EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values, Rand.RandSync.Unsynced); if (childSet != null) { CheckEventSet(stats, childSet, filter); @@ -398,7 +357,7 @@ namespace Barotrauma { foreach (var eventPrefab in thisSet.EventPrefabs) { - AddEvents(stats, eventPrefab.Prefabs, filter); + AddEvents(stats, eventPrefab.EventPrefabs, filter); } foreach (var childSet in thisSet.ChildSets) { @@ -419,7 +378,7 @@ namespace Barotrauma if (Rand.Value() > spawnProbability) { return; } int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } - string character = monsterEvent.speciesName; + Identifier character = monsterEvent.SpeciesName; if (stats.MonsterCounts.TryGetValue(character, out int currentCount)) { if (currentCount >= monsterEvent.MaxAmountPerLevel) { return; } @@ -430,7 +389,7 @@ namespace Barotrauma } stats.MonsterCounts[character] += count; - var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai"); + var aiElement = CharacterPrefab.FindBySpeciesName(character)?.ConfigElement?.GetChildElement("ai"); if (aiElement != null) { stats.MonsterStrength += aiElement.GetAttributeFloat("combatstrength", 0) * count; @@ -447,7 +406,7 @@ namespace Barotrauma } else { - var allMonsters = new Dictionary(); + var allMonsters = new Dictionary(); foreach (var stat in stats) { foreach (var monster in stat.MonsterCounts) @@ -473,7 +432,7 @@ namespace Barotrauma } } - static string LogMonsterCounts(Dictionary stats, float divider = 0) + static string LogMonsterCounts(Dictionary stats, float divider = 0) { if (divider > 0) { @@ -485,5 +444,14 @@ namespace Barotrauma } } } + + public override void Dispose() + { + foreach (var childSet in ChildSets) + { + childSet.Dispose(); + } + + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs index 74f1b1e2a..f716173e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class MalfunctionEvent : Event { - private string[] targetItemIdentifiers; + private Identifier[] targetItemIdentifiers; private List targetItems; @@ -36,7 +36,7 @@ namespace Barotrauma decreaseConditionAmount = prefab.ConfigElement.GetAttributeFloat("decreaseconditionamount", 0.0f); duration = prefab.ConfigElement.GetAttributeFloat("duration", 0.0f); - targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("itemidentifiers", new string[0]); + targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty()); } public override bool CanAffectSubImmediately(Level level) @@ -47,11 +47,11 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)); - int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.Server); + int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient); for (int i = 0; i < itemAmount; i++) { if (matchingItems.Count == 0) break; - targetItems.Add(matchingItems[Rand.Int(matchingItems.Count, Rand.RandSync.Server)]); + targetItems.Add(matchingItems[Rand.Int(matchingItems.Count, Rand.RandSync.ServerAndClient)]); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 32a2d3c6c..9665b6b90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -23,7 +23,7 @@ namespace Barotrauma protected const int HostagesKilledState = 5; - private readonly string hostagesKilledMessage; + private readonly LocalizedString hostagesKilledMessage; private const float EndDelay = 5.0f; private float endTimer; @@ -81,12 +81,12 @@ namespace Barotrauma public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - characterConfig = prefab.ConfigElement.Element("Characters"); + characterConfig = prefab.ConfigElement.GetChildElement("Characters"); string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", ""); - hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag; + hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag); - itemConfig = prefab.ConfigElement.Element("Items"); + itemConfig = prefab.ConfigElement.GetChildElement("Items"); itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); } @@ -139,14 +139,14 @@ namespace Barotrauma continue; } - string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); - string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); + Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); if (spawnPoint == null) { - spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); } Vector2 spawnPos = spawnPoint.WorldPosition; if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) @@ -194,7 +194,7 @@ namespace Barotrauma } else { - string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", "")); + Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty)); var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab == null) { @@ -212,8 +212,8 @@ namespace Barotrauma private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine) { - string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); - string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); + Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, moduleFlags ?? humanPrefab.GetModuleFlags(), @@ -221,7 +221,7 @@ namespace Barotrauma element.GetAttributeBool("asfaraspossible", false)); if (spawnPos == null) { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); } bool requiresRescue = element.GetAttributeBool("requirerescue", false); @@ -249,12 +249,12 @@ namespace Barotrauma private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine) { - string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); - string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); + Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); if (spawnPos == null) { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); } Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); characters.Add(spawnedCharacter); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 993b0cd02..78ddfaed8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -1,3 +1,4 @@ +using System; using Barotrauma.Extensions; using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; @@ -8,8 +9,8 @@ namespace Barotrauma { partial class AlienRuinMission : Mission { - private readonly string[] targetItemIdentifiers; - private readonly string[] targetEnemyIdentifiers; + private readonly Identifier[] targetItemIdentifiers; + private readonly Identifier[] targetEnemyIdentifiers; private readonly int minEnemyCount; private readonly HashSet existingTargets = new HashSet(); private readonly HashSet spawnedTargets = new HashSet(); @@ -34,8 +35,8 @@ namespace Barotrauma public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetitems", new string[0], convertToLowerInvariant: true); - targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetenemies", new string[0], convertToLowerInvariant: true); + targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetitems", Array.Empty()); + targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetenemies", Array.Empty()); minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0); } @@ -45,7 +46,7 @@ namespace Barotrauma spawnedTargets.Clear(); allTargets.Clear(); if (IsClient) { return; } - TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins"); @@ -66,8 +67,8 @@ namespace Barotrauma int existingEnemyCount = 0; foreach (var character in Character.CharacterList) { - if (string.IsNullOrEmpty(character.SpeciesName)) { continue; } - if (!targetEnemyIdentifiers.Contains(character.SpeciesName.ToLowerInvariant())) { continue; } + if (character.SpeciesName.IsEmpty) { continue; } + if (!targetEnemyIdentifiers.Contains(character.SpeciesName)) { continue; } if (character.Submarine != TargetRuin.Submarine) { continue; } existingTargets.Add(character); allTargets.Add(character); @@ -76,7 +77,7 @@ namespace Barotrauma if (existingEnemyCount < minEnemyCount) { var enemyPrefabs = new HashSet(); - foreach (string identifier in targetEnemyIdentifiers) + foreach (Identifier identifier in targetEnemyIdentifiers) { var prefab = CharacterPrefab.FindBySpeciesName(identifier); if (prefab != null) @@ -95,8 +96,8 @@ namespace Barotrauma } for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++) { - var prefab = enemyPrefabs.GetRandom(); - var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandom(w => w.CurrentHull != null)?.WorldPosition; + var prefab = enemyPrefabs.GetRandomUnsynced(); + var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition; if (!spawnPos.HasValue) { DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index d5ee46335..6b09ce371 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -9,18 +9,48 @@ namespace Barotrauma { partial class BeaconMission : Mission { + private class MonsterSet + { + public readonly HashSet<(CharacterPrefab character, Point amountRange)> MonsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>(); + public float Commonness; + + public MonsterSet(XElement element) + { + Commonness = element.GetAttributeFloat("commonness", 100.0f); + } + } + private bool swarmSpawned; - private readonly string monsterSpeciesName; - private Point monsterCountRange; - private readonly string sonarLabel; + private readonly List monsterSets = new List(); + private readonly LocalizedString sonarLabel; public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { swarmSpawned = false; - XElement monsterElement = prefab.ConfigElement.Element("monster"); + foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) + { + if (!monsterSets.Any()) + { + monsterSets.Add(new MonsterSet(monsterElement)); + } + LoadMonsters(monsterElement, monsterSets[0]); + } + foreach (var monsterSetElement in prefab.ConfigElement.GetChildElements("monsters")) + { + monsterSets.Add(new MonsterSet(monsterSetElement)); + foreach (var monsterElement in monsterSetElement.GetChildElements("monster")) + { + LoadMonsters(monsterElement, monsterSets.Last()); + } + } - monsterSpeciesName = monsterElement.GetAttributeString("character", string.Empty); + sonarLabel = TextManager.Get("beaconstationsonarlabel"); + } + + private void LoadMonsters(XElement monsterElement, MonsterSet set) + { + Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) { @@ -28,17 +58,22 @@ namespace Barotrauma } int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255); int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255); - - monsterCountRange = new Point(min, max); - - sonarLabel = TextManager.Get("beaconstationsonarlabel"); + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (characterPrefab != null) + { + set.MonsterPrefabs.Add((characterPrefab, new Point(min, max))); + } + else + { + DebugConsole.ThrowError($"Error in beacon mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } } - public override string SonarLabel + public override LocalizedString SonarLabel { get { - return string.IsNullOrEmpty(base.SonarLabel) ? sonarLabel : base.SonarLabel; + return base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel; } } @@ -101,16 +136,21 @@ namespace Barotrauma } } - int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); - for (int i = 0; i < amount; i++) + var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced); + foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs) { - CoroutineManager.Invoke(() => + int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); + for (int i = 0; i < amount; i++) { - //round ended before the coroutine finished - if (GameMain.GameSession == null || Level.Loaded == null) { return; } - Entity.Spawner.AddToSpawnQueue(monsterSpeciesName, spawnPos); - }, Rand.Range(0f, amount)); + CoroutineManager.Invoke(() => + { + //round ended before the coroutine finished + if (GameMain.GameSession == null || Level.Loaded == null) { return; } + Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos); + }, Rand.Range(0f, amount)); + } } + swarmSpawned = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index ce5311135..934b9df66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -9,24 +9,25 @@ namespace Barotrauma { partial class CargoMission : Mission { - private readonly XElement itemConfig; + private readonly ContentXElement itemConfig; private readonly List items = new List(); private readonly Dictionary parentInventoryIDs = new Dictionary(); + private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); private float requiredDeliveryAmount; - private readonly List<(XElement element, ItemContainer container)> itemsToSpawn = new List<(XElement element, ItemContainer container)>(); + private readonly List<(ContentXElement element, ItemContainer container)> itemsToSpawn = new List<(ContentXElement element, ItemContainer container)>(); private int? rewardPerCrate; private int calculatedReward; private int maxItemCount; private Submarine sub; - + private readonly List previouslySelectedMissions = new List(); - public override string Description + public override LocalizedString Description { get { @@ -43,7 +44,7 @@ namespace Barotrauma : base(prefab, locations, sub) { this.sub = sub; - itemConfig = prefab.ConfigElement.Element("Items"); + itemConfig = prefab.ConfigElement.GetChildElement("Items"); requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f); //this can get called between rounds when the client receives a campaign save //don't attempt to determine cargo if the sub hasn't been fully loaded @@ -91,7 +92,7 @@ namespace Barotrauma } maxItemCount = 0; - foreach (XElement subElement in itemConfig.Elements()) + foreach (var subElement in itemConfig.Elements()) { int maxCount = subElement.GetAttributeInt("maxcount", 10); maxItemCount += maxCount; @@ -99,7 +100,7 @@ namespace Barotrauma for (int i = 0; i < containers.Count; i++) { - foreach (XElement subElement in itemConfig.Elements()) + foreach (var subElement in itemConfig.Elements()) { int maxCount = subElement.GetAttributeInt("maxcount", 10); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } @@ -183,6 +184,7 @@ namespace Barotrauma items.Clear(); parentInventoryIDs.Clear(); parentItemContainerIndices.Clear(); + inventorySlotIndices.Clear(); if (itemConfig == null) { @@ -198,7 +200,7 @@ namespace Barotrauma if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; } } - private void LoadItemAsChild(XElement element, Item parent) + private void LoadItemAsChild(ContentXElement element, Item parent) { ItemPrefab itemPrefab = FindItemPrefab(element); @@ -218,9 +220,10 @@ namespace Barotrauma parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(parent.GetComponent())); parent.Combine(item, user: null); + inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { int amount = subElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 30a7c608c..9e2e4c921 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -6,8 +6,8 @@ namespace Barotrauma { private Submarine[] subs; - private readonly string[] descriptions; - private static string[] teamNames = { "Team A", "Team B" }; + private readonly LocalizedString[] descriptions; + private static LocalizedString[] teamNames = { "Team A", "Team B" }; public override bool AllowRespawn { @@ -23,11 +23,11 @@ namespace Barotrauma } } - public override string SuccessMessage + public override LocalizedString SuccessMessage { get { - if (Winner == CharacterTeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; } + if (Winner == CharacterTeamType.None || base.SuccessMessage.IsNullOrEmpty()) { return ""; } //disable success message for now if it hasn't been translated if (!TextManager.ContainsTag("MissionSuccess." + Prefab.TextIdentifier)) { return ""; } @@ -45,11 +45,11 @@ namespace Barotrauma public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - descriptions = new string[] + descriptions = new LocalizedString[] { - TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("descriptionneutral", ""), - TextManager.Get("MissionDescription1." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("description1", ""), - TextManager.Get("MissionDescription2." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("description2", "") + TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("descriptionneutral", "")), + TextManager.Get("MissionDescription1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("description1", "")), + TextManager.Get("MissionDescription2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("description2", "")) }; for (int i = 0; i < descriptions.Length; i++) @@ -60,14 +60,14 @@ namespace Barotrauma } } - teamNames = new string[] + teamNames = new LocalizedString[] { - TextManager.Get("MissionTeam1." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname1", "Team A"), - TextManager.Get("MissionTeam2." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname2", "Team B") + TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname1", "Team A")), + TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname2", "Team B")) }; } - public static string GetTeamName(CharacterTeamType teamID) + public static LocalizedString GetTeamName(CharacterTeamType teamID) { if (teamID == CharacterTeamType.Team1) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 99bdf971e..a1434ba45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -34,11 +34,11 @@ namespace Barotrauma : base(prefab, locations, sub) { missionSub = sub; - characterConfig = prefab.ConfigElement.Element("Characters"); + characterConfig = prefab.ConfigElement.GetChildElement("Characters"); baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1); scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); - itemConfig = prefab.ConfigElement.Element("TerroristItems"); + itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems"); CalculateReward(); } @@ -87,7 +87,7 @@ namespace Barotrauma characterItems.Clear(); WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); - Rand.RandSync randSync = Rand.RandSync.Server; + Rand.RandSync randSync = Rand.RandSync.ServerAndClient; if (terroristChance > 0f) { @@ -226,8 +226,8 @@ namespace Barotrauma if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands) { character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true)); - character.Speak(TextManager.Get("dialogterroristannounce"), null, Rand.Range(0.5f, 3f)); - XElement randomElement = itemConfig.Elements().GetRandom(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); + character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); if (randomElement != null) { HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true); @@ -286,7 +286,8 @@ namespace Barotrauma private bool Survived(Character character) { - return IsAlive(character) && character.CurrentHull != null && character.CurrentHull.Submarine == Submarine.MainSub; + return IsAlive(character) && character.CurrentHull?.Submarine != null && + (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)); } private bool IsAlive(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index e407ba68d..c4a787a40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -9,10 +9,23 @@ namespace Barotrauma { partial class MineralMission : Mission { - private readonly Dictionary resourceClusters = new Dictionary(); - private readonly Dictionary> spawnedResources = new Dictionary>(); - private readonly Dictionary relevantLevelResources = new Dictionary(); - private readonly List> missionClusterPositions = new List>(); + private struct ResourceCluster + { + public int Amount; + public float Rotation; + + public ResourceCluster(int amount, float rotation) + { + Amount = amount; + Rotation = rotation; + } + + public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation); + } + private readonly Dictionary resourceClusters = new Dictionary(); + private readonly Dictionary> spawnedResources = new Dictionary>(); + private readonly Dictionary relevantLevelResources = new Dictionary(); + private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>(); private readonly HashSet caves = new HashSet(); @@ -28,14 +41,14 @@ namespace Barotrauma public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - var configElement = prefab.ConfigElement.Element("Items"); + var configElement = prefab.ConfigElement.GetChildElement("Items"); foreach (var c in configElement.GetChildElements("Item")) { - var identifier = c.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) { continue; } + var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) { continue; } if (resourceClusters.ContainsKey(identifier)) { - resourceClusters[identifier] = (resourceClusters[identifier].amount + 1, resourceClusters[identifier].rotation); + resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation); } else { @@ -88,11 +101,11 @@ namespace Barotrauma "couldn't find an item prefab with the identifier " + kvp.Key); continue; } - var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.amount, out float rotation); - if (spawnedResources.Count < kvp.Value.amount) + var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.Amount, out float rotation); + if (spawnedResources.Count < kvp.Value.Amount) { DebugConsole.ThrowError("Error in MineralMission - " + - "spawned " + spawnedResources.Count + "/" + kvp.Value.amount + " of " + prefab.Name); + "spawned " + spawnedResources.Count + "/" + kvp.Value.Amount + " of " + prefab.Name); } if (spawnedResources.None()) { continue; } this.spawnedResources.Add(kvp.Key, spawnedResources); @@ -176,8 +189,8 @@ namespace Barotrauma { if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) { - var collected = availableResources.Count(r => HasBeenCollected(r)); - var needed = kvp.Value.amount; + var collected = availableResources.Count(HasBeenCollected); + var needed = kvp.Value.Amount; if (collected < needed) { return false; } } else @@ -221,7 +234,7 @@ namespace Barotrauma itemCount++; } pos /= itemCount; - missionClusterPositions.Add(new Tuple(kvp.Key, pos)); + missionClusterPositions.Add((kvp.Key, pos)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 097d2ca16..0bccbf953 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -3,6 +3,7 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -37,36 +38,33 @@ namespace Barotrauma protected bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; - public readonly List Headers; - public readonly List Messages; - - public string Name - { - get { return Prefab.Name; } - } + public readonly ImmutableArray Headers; + public readonly ImmutableArray Messages; - private readonly string successMessage; - public virtual string SuccessMessage + public LocalizedString Name => Prefab.Name; + + private readonly LocalizedString successMessage; + public virtual LocalizedString SuccessMessage { get { return successMessage; } //private set { successMessage = value; } } - private readonly string failureMessage; - public virtual string FailureMessage + private readonly LocalizedString failureMessage; + public virtual LocalizedString FailureMessage { get { return failureMessage; } //private set { failureMessage = value; } } - protected string description; - public virtual string Description + protected LocalizedString description; + public virtual LocalizedString Description { get { return description; } //private set { description = value; } } - protected string descriptionWithoutReward; + protected LocalizedString descriptionWithoutReward; public virtual bool AllowUndocking { @@ -81,7 +79,7 @@ namespace Barotrauma } } - public Dictionary ReputationRewards + public Dictionary ReputationRewards { get { return Prefab.ReputationRewards; } } @@ -116,15 +114,10 @@ namespace Barotrauma { get { return Enumerable.Empty(); } } - - public virtual string SonarLabel - { - get { return Prefab.SonarLabel; } - } - public string SonarIconIdentifier - { - get { return Prefab.SonarIconIdentifier; } - } + + public virtual LocalizedString SonarLabel => Prefab.SonarLabel; + + public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier; public readonly Location[] Locations; @@ -155,11 +148,11 @@ namespace Barotrauma Prefab = prefab; - description = prefab.Description; - successMessage = prefab.SuccessMessage; - failureMessage = prefab.FailureMessage; - Headers = new List(prefab.Headers); - Messages = new List(prefab.Messages); + description = prefab.Description.Value; + successMessage = prefab.SuccessMessage.Value; + failureMessage = prefab.FailureMessage.Value; + Headers = prefab.Headers; + var messages = prefab.Messages.ToArray(); Locations = locations; @@ -169,9 +162,9 @@ namespace Barotrauma if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); } if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); } if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); } - for (int m = 0; m < Messages.Count; m++) + for (int m = 0; m < messages.Length; m++) { - Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName); + messages[m] = messages[m].Replace("[location" + (n + 1) + "]", locationName); } } string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; @@ -182,10 +175,12 @@ namespace Barotrauma } if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); } if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); } - for (int m = 0; m < Messages.Count; m++) + for (int m = 0; m < messages.Length; m++) { - Messages[m] = Messages[m].Replace("[reward]", rewardText); + messages[m] = messages[m].Replace("[reward]", rewardText); } + + Messages = messages.ToImmutableArray(); } public virtual void SetLevel(LevelData level) { } @@ -204,7 +199,7 @@ namespace Barotrauma } else { - allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.Type)) != 0)); + allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => ((int)(missionType & m.Type)) != 0)); } allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly); @@ -246,7 +241,7 @@ namespace Barotrauma delayedTriggerEvents.Clear(); foreach (string categoryToShow in Prefab.UnhideEntitySubCategories) { - foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false)) + foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false)) { entityToShow.HiddenInGame = false; } @@ -318,7 +313,7 @@ namespace Barotrauma private void TriggerEvent(MissionPrefab.TriggerEvent trigger) { if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } - var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier.Equals(trigger.EventIdentifier, StringComparison.OrdinalIgnoreCase)); + var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier); if (eventPrefab == null) { DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\")."); @@ -384,23 +379,23 @@ namespace Barotrauma int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); campaign.Money += totalReward; - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); foreach (Character character in crewCharacters) { character.Info.MissionsCompletedSinceDeath++; } - foreach (KeyValuePair reputationReward in ReputationRewards) + foreach (KeyValuePair reputationReward in ReputationRewards) { - if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase)) + if (reputationReward.Key == "location") { Locations[0].Reputation.AddReputation(reputationReward.Value); Locations[1].Reputation.AddReputation(reputationReward.Value); } else { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase)); + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } } } @@ -422,7 +417,7 @@ namespace Barotrauma int srcIndex = -1; for (int i = 0; i < Locations.Length; i++) { - if (Locations[i].Type.Identifier.Equals(change.CurrentType, StringComparison.OrdinalIgnoreCase)) + if (Locations[i].Type.Identifier == change.CurrentType) { srcIndex = i; break; @@ -437,7 +432,7 @@ namespace Barotrauma } else { - location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase))); + location.ChangeType(LocationType.Prefabs[change.ChangeToType]); location.LocationTypeChangeCooldown = change.CooldownAfterChange; } } @@ -455,8 +450,8 @@ namespace Barotrauma return null; } - string characterIdentifier = element.GetAttributeString("identifier", ""); - string characterFrom = element.GetAttributeString("from", ""); + Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty); HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { @@ -467,9 +462,9 @@ namespace Barotrauma return humanPrefab; } - protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.Server, bool giveTags = true) + protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true) { - var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.Server) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); + var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.ServerAndClient) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobOrJobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); characterInfo.TeamID = teamType; if (positionToStayIn == null) @@ -480,9 +475,9 @@ namespace Barotrauma } Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); - spawnedCharacter.Prefab = humanPrefab; + spawnedCharacter.HumanPrefab = humanPrefab; humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); - humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.Server, createNetworkEvents: false); + humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false); characters.Add(spawnedCharacter); characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); @@ -536,7 +531,7 @@ namespace Barotrauma cargoRoomSub = cargoRoom.Submarine; return new Vector2( - cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server), + cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.ServerAndClient), cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index c6b93aab0..8b6c7e1a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Xml.Linq; @@ -27,9 +28,9 @@ namespace Barotrauma All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins } - partial class MissionPrefab + partial class MissionPrefab : PrefabWithUintIdentifier { - public static readonly List List = new List(); + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static readonly Dictionary CoOpMissionClasses = new Dictionary() { @@ -52,15 +53,14 @@ namespace Barotrauma }; public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo }; - + private readonly ConstructorInfo constructor; public readonly MissionType Type; public readonly bool MultiplayerOnly, SingleplayerOnly; - public readonly string Identifier; - public readonly string TextIdentifier; + public readonly Identifier TextIdentifier; private readonly string[] tags; public IEnumerable Tags @@ -68,17 +68,19 @@ namespace Barotrauma get { return tags; } } - public readonly string Name; - public readonly string Description; - public readonly string SuccessMessage; - public readonly string FailureMessage; - public readonly string SonarLabel; - public readonly string SonarIconIdentifier; + public readonly LocalizedString Name; + public readonly LocalizedString Description; + public readonly LocalizedString SuccessMessage; + public readonly LocalizedString FailureMessage; + public readonly LocalizedString SonarLabel; + public readonly Identifier SonarIconIdentifier; - public readonly string AchievementIdentifier; + public readonly Identifier AchievementIdentifier; - public readonly Dictionary ReputationRewards = new Dictionary(); - public readonly List> DataRewards = new List>(); + public readonly Dictionary ReputationRewards = new Dictionary(); + + public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> + DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); public readonly int Commonness; public readonly int? Difficulty; @@ -86,8 +88,8 @@ namespace Barotrauma public readonly int Reward; - public readonly List Headers; - public readonly List Messages; + public readonly ImmutableArray Headers; + public readonly ImmutableArray Messages; public readonly bool AllowRetry; @@ -98,12 +100,12 @@ namespace Barotrauma /// /// The mission can only be received when travelling from a location of the first type to a location of the second type /// - public readonly List<(string from, string to)> AllowedConnectionTypes; + public readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes; /// /// The mission can only be received in these location types /// - public readonly List AllowedLocationTypes = new List(); + public readonly List AllowedLocationTypes = new List(); /// /// Show entities belonging to these sub categories when the mission starts @@ -112,16 +114,16 @@ namespace Barotrauma public class TriggerEvent { - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string EventIdentifier { get; private set; } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int State { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Delay { get; private set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool CampaignOnly { get; private set; } public TriggerEvent(XElement element) @@ -134,66 +136,18 @@ namespace Barotrauma public LocationTypeChange LocationTypeChangeOnCompleted; - public readonly XElement ConfigElement; + public readonly ContentXElement ConfigElement; - public static void Init() - { - List.Clear(); - var files = GameMain.Instance.GetFilesOfType(ContentType.Missions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - bool allowOverride = false; - var mainElement = doc.Root; - if (mainElement.IsOverride()) - { - allowOverride = true; - mainElement = mainElement.FirstElement(); - } - - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; - var identifier = element.GetAttributeString("identifier", string.Empty); - var duplicate = List.Find(m => m.Identifier == identifier); - if (duplicate != null) - { - if (allowOverride || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding a mission with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow); - List.Remove(duplicate); - } - else - { - DebugConsole.ThrowError($"Duplicate mission found with the identifier '{identifier}' in file '{file.Path}'! Add tags as the parent of the mission definition to allow overriding."); - // TODO: Don't allow adding duplicates when the issue with multiple missions is solved. - //continue; - } - } - List.Add(new MissionPrefab(element)); - } - } - } - - public MissionPrefab(XElement element) + public MissionPrefab(ContentXElement element, MissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { ConfigElement = element; - Identifier = element.GetAttributeString("identifier", ""); - TextIdentifier = element.GetAttributeString("textidentifier", null) ?? Identifier; + TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier); - tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); - Name = TextManager.Get("MissionName." + TextIdentifier, true); - if (Name == null) - { -#if DEBUG - DebugConsole.ThrowError($"Error in mission \"{Identifier}\" - could not find a name in localization files. Make sure the texts are present in the loca file or that the mission is set to share texts with another mission using the TextIdentifier attribute."); -#endif - Name = element.GetAttributeString("name", ""); - } - Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", ""); + Name = TextManager.Get($"MissionName.{TextIdentifier}").Fallback(element.GetAttributeString("name", "")); + Description = TextManager.Get($"MissionDescription.{TextIdentifier}").Fallback(element.GetAttributeString("description", "")); Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); IsSideObjective = element.GetAttributeBool("sideobjective", false); @@ -205,87 +159,73 @@ namespace Barotrauma Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } - SuccessMessage = TextManager.Get("MissionSuccess." + TextIdentifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully"); - FailureMessage = TextManager.Get("MissionFailure." + TextIdentifier, true) ?? ""; - if (string.IsNullOrEmpty(FailureMessage) && TextManager.ContainsTag("missionfailed")) - { - FailureMessage = TextManager.Get("missionfailed", returnNull: true) ?? ""; - } - if (string.IsNullOrEmpty(FailureMessage) && GameMain.Config.Language == "English") - { - FailureMessage = element.GetAttributeString("failuremessage", ""); - } + SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}").Fallback(element.GetAttributeString("successmessage", "Mission completed successfully")); + FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}").Fallback( + TextManager.Get("missionfailed")).Fallback( + GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : ""); - if (element.Attribute("sonarlabel") == null) - { - SonarLabel = - TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? - TextManager.Get("missionsonarlabel.target"); - } - else - { - SonarLabel = - TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ?? - TextManager.Get(element.GetAttributeString("sonarlabel", ""), true) ?? - element.GetAttributeString("sonarlabel", ""); - } - - SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); + SonarLabel = + TextManager.Get($"MissionSonarLabel.{TextIdentifier}").Fallback( + TextManager.Get($"MissionSonarLabel.{element.GetAttributeString("sonarlabel", "")}")).Fallback( + element.GetAttributeString("sonarlabel", "")); + SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", ""); MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false); - AchievementIdentifier = element.GetAttributeString("achievementidentifier", ""); + AchievementIdentifier = element.GetAttributeIdentifier("achievementidentifier", ""); - UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", new string[0]).ToList(); + UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", Array.Empty()).ToList(); - Headers = new List(); - Messages = new List(); - AllowedConnectionTypes = new List<(string from, string to)>(); + var headers = new List(); + var messages = new List(); + AllowedConnectionTypes = new List<(Identifier from, Identifier to)>(); for (int i = 0; i < 100; i++) { - string header = TextManager.Get("MissionHeader" + i + "." + TextIdentifier, true); - string message = TextManager.Get("MissionMessage" + i + "." + TextIdentifier, true); - if (!string.IsNullOrEmpty(message)) + LocalizedString header = TextManager.Get($"MissionHeader{i}.{TextIdentifier}"); + LocalizedString message = TextManager.Get($"MissionMessage{i}.{TextIdentifier}"); + if (!message.IsNullOrEmpty()) { - Headers.Add(header); - Messages.Add(message); + headers.Add(header); + messages.Add(message); } } - + int messageIndex = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "message": - if (messageIndex > Headers.Count - 1) + if (messageIndex > headers.Count - 1) { - Headers.Add(string.Empty); - Messages.Add(string.Empty); + headers.Add(string.Empty); + messages.Add(string.Empty); } - Headers[messageIndex] = TextManager.Get("MissionHeader" + messageIndex + "." + TextIdentifier, true) ?? subElement.GetAttributeString("header", ""); - Messages[messageIndex] = TextManager.Get("MissionMessage" + messageIndex + "." + TextIdentifier, true) ?? subElement.GetAttributeString("text", ""); + headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("header", "")); + messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("text", "")); messageIndex++; break; case "locationtype": case "connectiontype": if (subElement.Attribute("identifier") != null) { - AllowedLocationTypes.Add(subElement.GetAttributeString("identifier", "")); + AllowedLocationTypes.Add(subElement.GetAttributeIdentifier("identifier", "")); } else { - AllowedConnectionTypes.Add((subElement.GetAttributeString("from", "").ToLowerInvariant(), subElement.GetAttributeString("to", "").ToLowerInvariant())); + AllowedConnectionTypes.Add(( + subElement.GetAttributeIdentifier("from", ""), + subElement.GetAttributeIdentifier("to", ""))); } break; case "locationtypechange": - LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeString("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f); + LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeIdentifier("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f); break; case "reputation": case "reputationreward": - string factionIdentifier = subElement.GetAttributeString("identifier", ""); + Identifier factionIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); float amount = subElement.GetAttributeFloat("amount", 0.0f); if (ReputationRewards.ContainsKey(factionIdentifier)) { @@ -293,18 +233,11 @@ namespace Barotrauma continue; } ReputationRewards.Add(factionIdentifier, amount); - if (!factionIdentifier.Equals("location", StringComparison.OrdinalIgnoreCase)) - { - if (FactionPrefab.Prefabs != null && !FactionPrefab.Prefabs.Any(p => p.Identifier.Equals(factionIdentifier, StringComparison.OrdinalIgnoreCase))) - { - DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Could not find a faction with the identifier \"{factionIdentifier}\"."); - } - } break; case "metadata": - string identifier = subElement.GetAttributeString("identifier", string.Empty); + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); string stringValue = subElement.GetAttributeString("value", string.Empty); - if (!string.IsNullOrWhiteSpace(stringValue) && !string.IsNullOrWhiteSpace(identifier)) + if (!string.IsNullOrWhiteSpace(stringValue) && !identifier.IsEmpty) { object value = SetDataAction.ConvertXMLValue(stringValue); SetDataAction.OperationType operation = SetDataAction.OperationType.Set; @@ -315,7 +248,7 @@ namespace Barotrauma operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString); } - DataRewards.Add(Tuple.Create(identifier, value, operation)); + DataRewards.Add((identifier, value, operation)); } break; case "triggerevent": @@ -323,15 +256,17 @@ namespace Barotrauma break; } } + Headers = headers.ToImmutableArray(); + Messages = messages.ToImmutableArray(); - string missionTypeName = element.GetAttributeString("type", ""); + Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty); //backwards compatibility - if (missionTypeName.Equals("outpostdestroy", StringComparison.OrdinalIgnoreCase) || missionTypeName.Equals("outpostrescue", StringComparison.OrdinalIgnoreCase)) + if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue") { - missionTypeName = "AbandonedOutpost"; + missionTypeName = "AbandonedOutpost".ToIdentifier(); } - if (!Enum.TryParse(missionTypeName, out Type)) + if (!Enum.TryParse(missionTypeName.Value, true, out Type)) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); return; @@ -362,25 +297,25 @@ namespace Barotrauma InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public bool IsAllowed(Location from, Location to) { if (from == to) { return - AllowedLocationTypes.Any(lt => lt.Equals("any", StringComparison.OrdinalIgnoreCase)) || - AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)); + AllowedLocationTypes.Any(lt => lt == "any") || + AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); } - foreach ((string fromType, string toType) in AllowedConnectionTypes) + foreach (var (fromType, toType) in AllowedConnectionTypes) { - if (fromType.Equals("any", StringComparison.OrdinalIgnoreCase) || - fromType.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase) || + if (fromType == "any" || + fromType == from.Type.Identifier || (fromType == "anyoutpost" && from.HasOutpost())) { - if (toType.Equals("any", StringComparison.OrdinalIgnoreCase) || - toType.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase) || + if (toType == "any" || + toType == to.Type.Identifier || (toType == "anyoutpost" && to.HasOutpost())) { return true; @@ -406,5 +341,11 @@ namespace Barotrauma { return constructor?.Invoke(new object[] { this, locations, sub }) as Mission; } + + partial void DisposeProjectSpecific(); + public override void Dispose() + { + DisposeProjectSpecific(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 31109da8e..ec2aea487 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -36,8 +36,8 @@ namespace Barotrauma public MonsterMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - string speciesName = prefab.ConfigElement.GetAttributeString("monsterfile", null); - if (!string.IsNullOrEmpty(speciesName)) + Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("monsterfile", Identifier.Empty); + if (!speciesName.IsEmpty) { var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab != null) @@ -62,7 +62,7 @@ namespace Barotrauma foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { - speciesName = monsterElement.GetAttributeString("character", string.Empty); + speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) { @@ -83,10 +83,10 @@ namespace Barotrauma if (monsterPrefabs.Any()) { - var characterParams = new CharacterParams(monsterPrefabs.First().character.FilePath); + var characterParams = new CharacterParams(monsterPrefabs.First().character.ContentFile as CharacterFile); description = description.Replace("[monster]", - TextManager.Get("character." + characterParams.SpeciesTranslationOverride, returnNull: true) ?? - TextManager.Get("character." + characterParams.SpeciesName)); + TextManager.Get("character." + characterParams.SpeciesTranslationOverride).Fallback( + TextManager.Get("character." + characterParams.SpeciesName))); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 210d5557c..5a85ee491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -11,7 +11,7 @@ namespace Barotrauma { partial class NestMission : Mission { - private readonly XElement itemConfig; + private readonly ContentXElement itemConfig; private readonly List items = new List(); private readonly Dictionary statusEffectOnApproach = new Dictionary(); @@ -49,7 +49,7 @@ namespace Barotrauma public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - itemConfig = prefab.ConfigElement.Element("Items"); + itemConfig = prefab.ConfigElement.GetChildElement("Items"); itemSpawnRadius = prefab.ConfigElement.GetAttributeFloat("itemspawnradius", 800.0f); approachItemsRadius = prefab.ConfigElement.GetAttributeFloat("approachitemsradius", itemSpawnRadius * 2.0f); @@ -69,7 +69,7 @@ namespace Barotrauma foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { - string speciesName = monsterElement.GetAttributeString("character", string.Empty); + Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) { @@ -170,7 +170,7 @@ namespace Barotrauma } } - foreach (XElement subElement in itemConfig.Elements()) + foreach (var subElement in itemConfig.Elements()) { string itemIdentifier = subElement.GetAttributeString("identifier", ""); if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) @@ -183,8 +183,8 @@ namespace Barotrauma float rotation = 0.0f; if (spawnEdges.Any()) { - var edge = spawnEdges.GetRandom(Rand.RandSync.Server); - spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.Server)); + var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient); + spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient)); Vector2 normal = Vector2.UnitY; if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) { @@ -204,10 +204,12 @@ namespace Barotrauma item.FindHull(); items.Add(item); - var statusEffectElement = subElement.Element("StatusEffectOnApproach") ?? subElement.Element("statuseffectonapproach"); + var statusEffectElement = + subElement.GetChildElement("StatusEffectOnApproach") + ?? subElement.GetChildElement("statuseffectonapproach"); if (statusEffectElement != null) { - statusEffectOnApproach.Add(item, StatusEffect.Load(statusEffectElement, Prefab.Identifier)); + statusEffectOnApproach.Add(item, StatusEffect.Load(statusEffectElement, Prefab.Identifier.Value)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index bc69390bf..6e0b0abc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -80,9 +80,9 @@ namespace Barotrauma public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - submarineTypeConfig = prefab.ConfigElement.Element("SubmarineTypes"); - characterConfig = prefab.ConfigElement.Element("Characters"); - characterTypeConfig = prefab.ConfigElement.Element("CharacterTypes"); + submarineTypeConfig = prefab.ConfigElement.GetChildElement("SubmarineTypes"); + characterConfig = prefab.ConfigElement.GetChildElement("Characters"); + characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes"); addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0); // for campaign missions, set level at construction @@ -111,21 +111,21 @@ namespace Barotrauma string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } - string submarinePath = submarineConfig.GetAttributeString("path", string.Empty); - if (submarinePath == string.Empty) + ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage); + if (submarinePath.IsNullOrEmpty()) { DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); return; } // maybe a little redundant - var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarinePath); + var contentFile = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(x => x.Path == submarinePath); if (contentFile == null) { DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); return; } - submarineInfo = new SubmarineInfo(contentFile.Path); + submarineInfo = new SubmarineInfo(contentFile.Path.Value); } private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) @@ -183,7 +183,7 @@ namespace Barotrauma var validNodes = path.Nodes.FindAll(n => !Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(n.WorldPosition)))); if (validNodes.Any()) { - preferredSpawnPos = validNodes.GetRandom().WorldPosition; // spawn the sub in a random point in the path if possible + preferredSpawnPos = validNodes.GetRandomUnsynced().WorldPosition; // spawn the sub in a random point in the path if possible } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 0bbb70d87..7526d1b25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -63,7 +63,7 @@ namespace Barotrauma string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null); if (itemIdentifier != null) { - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; } if (itemPrefab == null) { @@ -86,22 +86,22 @@ namespace Barotrauma spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; } - foreach (XElement element in prefab.ConfigElement.Elements()) + foreach (var element in prefab.ConfigElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { case "statuseffect": { - var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name); + var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name.Value); if (newEffect == null) { continue; } statusEffects.Add(new List { newEffect }); break; } case "chooserandom": statusEffects.Add(new List()); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name); + var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name.Value); if (newEffect == null) { continue; } statusEffects.Last().Add(newEffect); } @@ -200,12 +200,13 @@ namespace Barotrauma } if (validContainers.Any()) { - var selectedContainer = validContainers.GetRandom(Rand.RandSync.Unsynced); + var selectedContainer = validContainers.GetRandomUnsynced(); if (selectedContainer.Combine(item, user: null)) { #if SERVER originalInventoryID = selectedContainer.Item.ID; originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); + originalSlotIndex = item.ParentInventory?.FindIndex(item) ?? -1; #endif } // Placement successful } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index c84e43647..132767396 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -10,10 +10,11 @@ namespace Barotrauma { partial class ScanMission : Mission { - private readonly XElement itemConfig; + private readonly ContentXElement itemConfig; private readonly List startingItems = new List(); private readonly List scanners = new List(); private readonly Dictionary parentInventoryIDs = new Dictionary(); + private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); private readonly int targetsToScan; private readonly Dictionary scanTargets = new Dictionary(); @@ -55,7 +56,7 @@ namespace Barotrauma public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - itemConfig = prefab.ConfigElement.Element("Items"); + itemConfig = prefab.ConfigElement.GetChildElement("Items"); targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f); } @@ -78,7 +79,7 @@ namespace Barotrauma } GetScanners(); - TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins"); @@ -101,7 +102,7 @@ namespace Barotrauma availableWaypoints.AddRange(ruinWaypoints); for (int i = 0; i < targetsToScan; i++) { - var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.Server); + var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient); scanTargets.Add(selectedWaypoint, false); availableWaypoints.Remove(selectedWaypoint); if (i < (targetsToScan - 1)) @@ -143,6 +144,7 @@ namespace Barotrauma { startingItems.Clear(); parentInventoryIDs.Clear(); + inventorySlotIndices.Clear(); parentItemContainerIndices.Clear(); scanners.Clear(); TargetRuin = null; @@ -162,6 +164,7 @@ namespace Barotrauma parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer)); parent.Combine(item, user: null); + inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1); } foreach (XElement subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index bae60c6e4..4093d6923 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -9,8 +9,8 @@ namespace Barotrauma { class MonsterEvent : Event { - public readonly string speciesName; - public readonly int minAmount, maxAmount; + public readonly Identifier SpeciesName; + public readonly int MinAmount, MaxAmount; private List monsters; private readonly float scatter; @@ -31,8 +31,6 @@ namespace Barotrauma public List Monsters => monsters; public Vector2? SpawnPos => spawnPos; public bool SpawnPending => spawnPending; - public int MinAmount => minAmount; - public int MaxAmount => maxAmount; public override Vector2 DebugDrawPos { @@ -41,38 +39,42 @@ namespace Barotrauma public override string ToString() { - if (maxAmount <= 1) + if (MaxAmount <= 1) { - return $"MonsterEvent ({speciesName}, {SpawnPosType})"; + return $"MonsterEvent ({SpeciesName}, {SpawnPosType})"; } - else if (minAmount < maxAmount) + else if (MinAmount < MaxAmount) { - return $"MonsterEvent ({speciesName} x{minAmount}-{maxAmount}, {SpawnPosType})"; + return $"MonsterEvent ({SpeciesName} x{MinAmount}-{MaxAmount}, {SpawnPosType})"; } else { - return $"MonsterEvent ({speciesName} x{maxAmount}, {SpawnPosType})"; + return $"MonsterEvent ({SpeciesName} x{MaxAmount}, {SpawnPosType})"; } } public MonsterEvent(EventPrefab prefab) : base (prefab) { - speciesName = prefab.ConfigElement.GetAttributeString("characterfile", ""); - CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesName); + string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", ""); + CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile); if (characterPrefab != null) { - speciesName = characterPrefab.Identifier; + SpeciesName = characterPrefab.Identifier; + } + else + { + SpeciesName = speciesFile.ToIdentifier(); } - if (string.IsNullOrEmpty(speciesName)) + if (SpeciesName.IsEmpty) { throw new Exception("speciesname is null!"); } int defaultAmount = prefab.ConfigElement.GetAttributeInt("amount", 1); - minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount); - maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount); + MinAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount); + MaxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), MinAmount); MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); @@ -97,10 +99,10 @@ namespace Barotrauma if (GameMain.NetworkMember != null) { - List monsterNames = GameMain.NetworkMember.ServerSettings.MonsterEnabled.Keys.ToList(); - string tryKey = monsterNames.Find(s => speciesName.ToLower() == s.ToLower()); + List monsterNames = GameMain.NetworkMember.ServerSettings.MonsterEnabled.Keys.ToList(); + Identifier tryKey = monsterNames.Find(s => SpeciesName == s); - if (!string.IsNullOrWhiteSpace(tryKey)) + if (!tryKey.IsEmpty) { if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) { @@ -117,15 +119,15 @@ namespace Barotrauma public override IEnumerable GetFilesToPreload() { - string path = CharacterPrefab.FindBySpeciesName(speciesName)?.FilePath; - if (string.IsNullOrWhiteSpace(path)) + var file = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ContentFile; + if (file == null) { - DebugConsole.ThrowError($"Failed to find config file for species \"{speciesName}\""); + DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\""); yield break; } else { - yield return new ContentFile(path, ContentType.Character); + yield return file; } } @@ -137,9 +139,9 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { - DebugConsole.NewMessage("Initialized MonsterEvent (" + speciesName + ")", Color.White); + DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White); } } @@ -283,7 +285,7 @@ namespace Barotrauma Finished(); return; } - chosenPosition = availablePositions.GetRandom(); + chosenPosition = availablePositions.GetRandomUnsynced(); } if (chosenPosition.IsValid) { @@ -369,7 +371,7 @@ namespace Barotrauma { if (MaxAmountPerLevel < int.MaxValue) { - if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= MaxAmountPerLevel) + if (Character.CharacterList.Count(c => c.SpeciesName == SpeciesName) >= MaxAmountPerLevel) { disallowed = true; return; @@ -456,7 +458,7 @@ namespace Barotrauma spawnPending = false; //+1 because Range returns an integer less than the max value - int amount = Rand.Range(minAmount, maxAmount + 1); + int amount = Rand.Range(MinAmount, MaxAmount + 1); monsters = new List(); float scatterAmount = scatter; if (SpawnPosType.HasFlag(Level.PositionType.SidePath)) @@ -501,7 +503,7 @@ namespace Barotrauma } } - Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true); + Character createdCharacter = Character.Create(SpeciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true); var eventManager = GameMain.GameSession.EventManager; if (eventManager != null) { @@ -545,7 +547,7 @@ namespace Barotrauma if (GameMain.GameSession != null) { GameAnalyticsManager.AddDesignEvent( - $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"}:{SpawnPosType}:{speciesName}", + $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}", value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); } }, delayBetweenSpawns * i); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 5be24397f..c41676ff9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -7,9 +7,9 @@ namespace Barotrauma { class ScriptedEvent : Event { - private readonly Dictionary>> targetPredicates = new Dictionary>>(); + private readonly Dictionary>> targetPredicates = new Dictionary>>(); - private readonly Dictionary> cachedTargets = new Dictionary>(); + private readonly Dictionary> cachedTargets = new Dictionary>(); private int prevEntityCount; private int prevPlayerCount, prevBotCount; @@ -18,7 +18,7 @@ namespace Barotrauma public int CurrentActionIndex { get; private set; } public List Actions { get; } = new List(); - public Dictionary> Targets { get; } = new Dictionary>(); + public Dictionary> Targets { get; } = new Dictionary>(); public override string ToString() { @@ -27,7 +27,7 @@ namespace Barotrauma public ScriptedEvent(EventPrefab prefab) : base(prefab) { - foreach (XElement element in prefab.ConfigElement.Elements()) + foreach (var element in prefab.ConfigElement.Elements()) { if (element.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { @@ -49,7 +49,7 @@ namespace Barotrauma GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start"); } - public void AddTarget(string tag, Entity target) + public void AddTarget(Identifier tag, Entity target) { if (target == null) { @@ -74,7 +74,7 @@ namespace Barotrauma } } - public void AddTargetPredicate(string tag, Predicate predicate) + public void AddTargetPredicate(Identifier tag, Predicate predicate) { if (!targetPredicates.ContainsKey(tag)) { @@ -88,7 +88,7 @@ namespace Barotrauma } } - public IEnumerable GetTargets(string tag) + public IEnumerable GetTargets(Identifier tag) { if (cachedTargets.ContainsKey(tag)) { @@ -140,9 +140,9 @@ namespace Barotrauma return targetsToReturn; } - public void RemoveTag(string tag) + public void RemoveTag(Identifier tag) { - if (string.IsNullOrWhiteSpace(tag)) { return; } + if (tag.IsEmpty) { return; } if (Targets.ContainsKey(tag)) { Targets.Remove(tag); } if (cachedTargets.ContainsKey(tag)) { cachedTargets.Remove(tag); } if (targetPredicates.ContainsKey(tag)) { targetPredicates.Remove(tag); } @@ -224,7 +224,7 @@ namespace Barotrauma foreach (LocationConnection c in currLocation.Connections) { if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; } - if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase))) + if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier == t)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 89327c8f0..4039e31dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System; using System.Linq; +using System.Collections.Immutable; namespace Barotrauma.Extensions { @@ -9,69 +10,102 @@ namespace Barotrauma.Extensions /// /// Randomizes the collection (using OrderBy) and returns it. /// - public static IOrderedEnumerable Randomize(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T[] Randomize(this IList source, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - return source.OrderBy(i => Rand.Value(randSync)); + return source.OrderBy(i => Rand.Value(randSync)).ToArray(); } /// /// Randomizes the list in place without creating a new collection, using a Fisher-Yates-based algorithm. /// public static void Shuffle(this IList list, Rand.RandSync randSync = Rand.RandSync.Unsynced) + => list.Shuffle(Rand.GetRNG(randSync)); + + public static void Shuffle(this IList list, Random rng) { int n = list.Count; while (n > 1) { n--; - int k = Rand.Int(n + 1, randSync); + int k = rng.Next(n + 1); T value = list[k]; list[k] = list[n]; list[n] = value; } } - public static T GetRandom(this IEnumerable source, Func predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T GetRandom(this IReadOnlyList source, Func predicate, Rand.RandSync randSync) { if (predicate == null) { return GetRandom(source, randSync); } - return source.Where(predicate).GetRandom(randSync); + return source.Where(predicate).ToArray().GetRandom(randSync); } - public static T GetRandom(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) + /// + /// Gets a random element of a list using one of the synced random number generators. + /// It's recommended that you guarantee a deterministic order of the elements of the + /// input list via sorting. + /// + /// List to pick a random element from + /// Which RNG to use + /// A random item from the list. Return value should match between clients and + /// the server, if applicable. + public static T GetRandom(this IReadOnlyList source, Rand.RandSync randSync) { - if (source is IList list) + int count = source.Count; + return count == 0 ? default : source[Rand.Range(0, count, randSync)]; + } + + public static T GetRandom(this IReadOnlyList source, Random random) + { + int count = source.Count; + return count == 0 ? default : source[random.Next(0, count)]; + } + + // The reason these "GetRandomUnsynced" methods exist is because + // they can be used on all enumerables; GetRandom can only be used + // on lists as they can be sorted to guarantee a certain order. + public static T GetRandomUnsynced(this IEnumerable source, Func predicate) + { + if (predicate == null) { return GetRandomUnsynced(source); } + return source.Where(predicate).GetRandomUnsynced(); + } + + public static T GetRandomUnsynced(this IEnumerable source) + { + if (source is IReadOnlyList list) { - int count = list.Count; - return count == 0 ? default : list[Rand.Range(0, count, randSync)]; + return list.GetRandom(Rand.RandSync.Unsynced); } else { int count = source.Count(); - return count == 0 ? default : source.ElementAt(Rand.Range(0, count, randSync)); + return count == 0 ? default : source.ElementAt(Rand.Range(0, count, Rand.RandSync.Unsynced)); } } - public static T GetRandom(this IEnumerable source, Random random) + + public static T GetRandom(this IEnumerable source, Rand.RandSync randSync) + where T : PrefabWithUintIdentifier { - if (source is IList list) - { - int count = list.Count; - return count == 0 ? default : list[random.Next(0, count)]; - } - else - { - int count = source.Count(); - return count == 0 ? default : source.ElementAt(random.Next(0, count)); - } + return source.OrderBy(p => p.UintIdentifier).ToArray().GetRandom(randSync); } - public static T RandomElementByWeight(this IEnumerable source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T GetRandom(this IEnumerable source, Func predicate, Rand.RandSync randSync) + where T : PrefabWithUintIdentifier + { + return source.Where(predicate).OrderBy(p => p.UintIdentifier).ToArray().GetRandom(randSync); + } + + + public static T RandomElementByWeight(this IList source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) { float totalWeight = source.Sum(weightSelector); float itemWeightIndex = Rand.Range(0f, 1f, randSync) * totalWeight; float currentWeightIndex = 0; - foreach (T weightedItem in source) + for (int i = 0; i < source.Count; i++) { + T weightedItem = source[i]; float weight = weightSelector(weightedItem); currentWeightIndex += weight; @@ -107,6 +141,14 @@ namespace Barotrauma.Extensions } } + /// + /// Iterates over all elements in a given enumerable and discards the result. + /// + public static void Consume(this IEnumerable enumerable) + { + foreach (var _ in enumerable) { /* do nothing */ } + } + /// /// Shorthand for !source.Any(predicate) -> i.e. not any. /// @@ -155,6 +197,27 @@ namespace Barotrauma.Extensions if (value != null) { source.Add(value); } } + public static ImmutableDictionary ToImmutableDictionary(this IEnumerable<(TKey, TValue)> enumerable) + { + return enumerable.ToDictionary().ToImmutableDictionary(); + } + + public static Dictionary ToDictionary(this IEnumerable<(TKey, TValue)> enumerable) + { + var dictionary = new Dictionary(); + foreach (var (k,v) in enumerable) + { + dictionary.Add(k, v); + } + return dictionary; + } + + public static Dictionary ToMutable(this ImmutableDictionary immutableDictionary) + { + if (immutableDictionary == null) { return null; } + return new Dictionary(immutableDictionary); + } + /// /// Returns whether a given collection has at least a certain amount /// of elements for which the predicate returns true. @@ -172,6 +235,18 @@ namespace Barotrauma.Extensions return false; } + /// + /// Equivalent to LINQ's Enumerable.Concat. The main difference is that this + /// takes advantage of ICollection optimizations for Enumerable.Contains + /// and Enumerable.Count. + /// + /// + public static ICollection CollectionConcat(this IEnumerable self, IEnumerable other) + => new CollectionConcat(self, other); + + public static IReadOnlyList ListConcat(this IEnumerable self, IEnumerable other) + => new ListConcat(self, other); + /// /// Returns the maximum element in a given enumerable, or null if there /// aren't any elements in the input. @@ -188,6 +263,10 @@ namespace Barotrauma.Extensions return retVal; } + public static TOut? MaxOrNull(this IEnumerable enumerable, Func conversion) + where TOut : struct, IComparable + => enumerable.Select(conversion).MaxOrNull(); + public static int FindIndex(this IReadOnlyList list, Predicate predicate) { for (int i=0; i string.IsNullOrEmpty(s) ? fallback : s; + + public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s); + public static bool IsNullOrWhiteSpace(this string? s) => string.IsNullOrWhiteSpace(s); + public static bool IsNullOrEmpty(this ContentPath? p) => p?.IsNullOrEmpty() ?? true; + public static bool IsNullOrWhiteSpace(this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; + public static bool IsNullOrEmpty(this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); + public static bool IsNullOrWhiteSpace(this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); + public static bool IsNullOrEmpty(this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); + public static bool IsNullOrWhiteSpace(this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace(); + + public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) + => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs index cb128e00d..64fd0ab3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs @@ -24,9 +24,9 @@ namespace Barotrauma return new string(newString.SelectMany(str => str.ToCharArray()).ToArray()); } - public static string Remove(this string s, string substring) + public static string Remove(this string s, string substring, StringComparison comparisonType = StringComparison.Ordinal) { - return s.Replace(substring, string.Empty); + return s.Replace(substring, string.Empty, comparisonType); } public static string Remove(this string s, Func predicate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index e8d0be2ea..c44ded911 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -136,6 +136,10 @@ namespace Barotrauma public static void InitIfConsented() { + #if DEBUG + return; + #endif + if (!consentTextAvailable) { SetConsent(Consent.Unknown); @@ -225,8 +229,9 @@ namespace Barotrauma DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); break; default: - DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", new string[2] { "[statuscode]", "[statusdescription]" }, - new string[2] { response.StatusCode.ToString(), response.StatusDescription })); + DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", + ("[statuscode]", response.StatusCode.ToString()), + ("[statusdescription]", response.StatusDescription))); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 763f59a77..82a9f705e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -230,7 +230,7 @@ namespace Barotrauma private string GetAssemblyPath(string assemblyName) => Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, $"{assemblyName}.dll"); private bool resolvingDependency; @@ -361,12 +361,9 @@ namespace Barotrauma if (!SendUserStatistics) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; } - if (GameMain.Config.AllEnabledPackages != null) + if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) { - if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) - { - message = "[MODDED] " + message; - } + message = "[MODDED] " + message; } loadedImplementation?.AddErrorEvent(errorSeverity, message); @@ -480,10 +477,7 @@ namespace Barotrauma Md5Hash? exeHash = null; try { - using (var stream = File.OpenRead(exePath)) - { - exeHash = new Md5Hash(stream); - } + exeHash = Md5Hash.CalculateForFile(exePath, Md5Hash.StringHashOptions.BytePerfect); } catch (Exception e) { @@ -512,7 +506,7 @@ namespace Barotrauma loadedImplementation?.AddDesignEvent("Executable:" + GameMain.Version.ToString() + exeName + ":" - + ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":" + + (exeHash?.ShortRepresentation ?? "Unknown") + ":" + AssemblyInfo.GitRevision + ":" + buildConfiguration); } @@ -523,24 +517,21 @@ namespace Barotrauma return; } - if (GameMain.Config != null) + var allPackages = ContentPackageManager.EnabledPackages.All.ToList(); + if (allPackages?.Count > 0) { - var allPackages = GameMain.Config.AllEnabledPackages.ToList(); - if (allPackages?.Count > 0) + List packageNames = new List(); + foreach (ContentPackage cp in allPackages) { - List packageNames = new List(); - foreach (ContentPackage cp in allPackages) - { - string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); - sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); - packageNames.Add(sanitizedName); - loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); - } - packageNames.Sort(); - loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames)); + string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); + sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); + packageNames.Add(sanitizedName); + loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); } - loadedImplementation?.AddDesignEvent("Language:" + GameMain.Config.Language); + packageNames.Sort(); + loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames)); } + loadedImplementation?.AddDesignEvent("Language:" + GameSettings.CurrentConfig.Language); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index e10ee447a..d1e7c70d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -6,6 +6,10 @@ using Barotrauma.Extensions; namespace Barotrauma { + #warning TODO: This class needs some changes: + // - We shouldn't be iterating over MapEntityPrefab.List. It has no guarantee of any sort of order and becomes entirely unpredictable once you start adding mods. + // - Note: iterating over ItemPrefab.Prefabs would also be incorrect. Sorting by UintIdentifier is necessary for determinism. + // - SpawnItems and SpawnItem are named incorrectly. static class AutoItemPlacer { public static bool OutputDebugInfo = false; @@ -76,8 +80,8 @@ namespace Barotrauma int itemCountApprox = MapEntityPrefab.List.Count() / 3; var containers = new List(70 + 30 * subs.Count()); - var prefabsWithContainer = new List(itemCountApprox / 3); - var prefabsWithoutContainer = new List(itemCountApprox); + var prefabsItemsCanSpawnIn = new List(itemCountApprox / 3); + var singlePrefabs = new List(itemCountApprox); var removals = new List(); // generate loot only for a specific container if defined @@ -93,42 +97,45 @@ namespace Barotrauma if (item.GetRootInventoryOwner() is Character) { continue; } containers.AddRange(item.GetComponents()); } - containers.Shuffle(Rand.RandSync.Server); + containers.Shuffle(Rand.RandSync.ServerAndClient); } - foreach (MapEntityPrefab prefab in MapEntityPrefab.List) + foreach (ItemPrefab ip in ItemPrefab.Prefabs) { - if (!(prefab is ItemPrefab ip)) { continue; } - - if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase))) + if (!ip.PreferredContainers.Any()) { continue; } + if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && + ItemPrefab.Prefabs.Any(ip2 => CanSpawnIn(ip2, ip))) { - prefabsWithContainer.Add(ip); + prefabsItemsCanSpawnIn.Add(ip); } else { - prefabsWithoutContainer.Add(ip); + singlePrefabs.Add(ip); } } - var validContainers = new Dictionary(); - prefabsWithContainer.Shuffle(Rand.RandSync.Server); - // Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) - for (int i = 0; i < prefabsWithContainer.Count; i++) + bool CanSpawnIn(ItemPrefab item, ItemPrefab container) { - var itemPrefab = prefabsWithContainer[i]; - if (itemPrefab == null) { continue; } - if (SpawnItems(itemPrefab)) + foreach (var preferredContainer in item.PreferredContainers) { - removals.Add(itemPrefab); + if (ItemPrefab.IsContainerPreferred(preferredContainer.Primary, container.Identifier.ToEnumerable().Union(container.Tags))) { return true; } } + return false; } - // Remove containers that we successfully spawned items into so that they are not counted in in the second pass. - removals.ForEach(i => prefabsWithContainer.Remove(i)); - // Another pass for items with containers because also they can spawn inside other items (like smg magazine) - prefabsWithContainer.ForEach(i => SpawnItems(i)); - // Spawn items that don't have containers last - prefabsWithoutContainer.Shuffle(Rand.RandSync.Server); - prefabsWithoutContainer.ForEach(i => SpawnItems(i)); + + var validContainers = new Dictionary(); + prefabsItemsCanSpawnIn.Shuffle(Rand.RandSync.ServerAndClient); + // Spawn items that other items can spawn in first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) + for (int i = 0; i < prefabsItemsCanSpawnIn.Count; i++) + { + var itemPrefab = prefabsItemsCanSpawnIn[i]; + if (itemPrefab == null) { continue; } + SpawnItems(itemPrefab); + } + + // Spawn items that nothing can spawn in last + singlePrefabs.Shuffle(Rand.RandSync.ServerAndClient); + singlePrefabs.ForEach(i => SpawnItems(i)); if (OutputDebugInfo) { @@ -158,12 +165,17 @@ namespace Barotrauma } } } -#if SERVER foreach (Item spawnedItem in spawnedItems) { +#if SERVER Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false); - } #endif + foreach (ItemComponent ic in spawnedItem.Components) + { + ic.OnItemLoaded(); + } + } + bool SpawnItems(ItemPrefab itemPrefab) { if (itemPrefab == null) @@ -229,13 +241,13 @@ namespace Barotrauma private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer, float difficultyModifier) { List spawnedItems = new List(); - if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; } + if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; } // Don't add dangerously reactive materials in thalamus wrecks if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) { return spawnedItems; } - int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server); + int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient); for (int i = 0; i < amount; i++) { if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true)) @@ -244,15 +256,15 @@ namespace Barotrauma break; } - var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab); int quality = existingItem?.Quality ?? ToolBox.SelectWeightedRandom( qualityCommonnesses.Select(q => q.quality).ToList(), qualityCommonnesses.Select(q => q.commonness).ToList(), - Rand.RandSync.Server); + Rand.RandSync.ServerAndClient); if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; } - var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine) + var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false) { SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost, AllowStealing = validContainer.Key.Item.AllowStealing, diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 6b20e104d..234bbefa9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -217,7 +217,7 @@ namespace Barotrauma // Exchange money var itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.Money -= itemValue; - GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier); + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); Location.StoreCurrentBalance += itemValue; if (removeFromCrate) @@ -367,7 +367,14 @@ namespace Barotrauma if (sub == Submarine.MainSub) { #if CLIENT - new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon"); + new GUIMessageBox("", + TextManager.GetWithVariable("CargoSpawnNotification", + "[roomname]", + cargoRoom.DisplayName, + FormatCapitals.Yes), + Array.Empty(), + type: GUIMessageBox.Type.InGame, + iconStyle: "StoreShoppingCrateIcon"); #else foreach (Client client in GameMain.Server.ConnectedClients) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 4856ff843..6e38a47f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -26,7 +26,17 @@ namespace Barotrauma public bool HasBots { get; set; } - public List> ActiveOrders { get; } = new List>(); + public class ActiveOrder + { + public readonly Order Order; + public float? FadeOutTime; + public ActiveOrder(Order order, float? fadeOutTime) + { + Order = order; + FadeOutTime = fadeOutTime; + } + } + public List ActiveOrders { get; } = new List(); public bool IsSinglePlayer { get; private set; } public ReadyCheck ActiveReadyCheck; @@ -54,16 +64,16 @@ namespace Barotrauma // Ignore orders work a bit differently since the "unignore" order counters the "ignore" order var isUnignoreOrder = order.Identifier == "unignorethis"; - var orderPrefab = !isUnignoreOrder ? order.Prefab : Order.GetPrefab("ignorethis"); - Pair existingOrder = ActiveOrders.Find(o => - o.First.Prefab == orderPrefab && MatchesTarget(o.First.TargetEntity, order.TargetEntity) && - (o.First.TargetType != Order.OrderTargetType.WallSection || o.First.WallSectionIndex == order.WallSectionIndex)); + var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs["ignorethis"]; + ActiveOrder existingOrder = ActiveOrders.Find(o => + o.Order.Prefab == orderPrefab && MatchesTarget(o.Order.TargetEntity, order.TargetEntity) && + (o.Order.TargetType != Order.OrderTargetType.WallSection || o.Order.WallSectionIndex == order.WallSectionIndex)); if (existingOrder != null) { if (!isUnignoreOrder) { - existingOrder.Second = fadeOutTime; + existingOrder.FadeOutTime = fadeOutTime; return false; } else @@ -74,7 +84,7 @@ namespace Barotrauma } else if (!isUnignoreOrder) { - ActiveOrders.Add(new Pair(order, fadeOutTime)); + ActiveOrders.Add(new ActiveOrder(order, fadeOutTime)); #if CLIENT HintManager.OnActiveOrderAdded(order); #endif @@ -96,7 +106,7 @@ namespace Barotrauma public void AddCharacterElements(XElement element) { - foreach (XElement characterElement in element.Elements()) + foreach (var characterElement in element.Elements()) { if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } CharacterInfo characterInfo = new CharacterInfo(characterElement); @@ -105,7 +115,7 @@ namespace Barotrauma characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1); #endif characterInfos.Add(characterInfo); - foreach (XElement subElement in characterElement.Elements()) + foreach (var subElement in characterElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -205,7 +215,7 @@ namespace Barotrauma wp.SpawnType == SpawnType.Human && wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull != null && - wp.CurrentHull.OutpostModuleTags.Contains("airlock")); + wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); @@ -236,7 +246,7 @@ namespace Barotrauma } if (character.Info.InventoryData != null) { - character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData); + character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null)); } else if (!character.Info.StartItemsGiven) { @@ -301,12 +311,12 @@ namespace Barotrauma public void Update(float deltaTime) { - foreach (Pair order in ActiveOrders) + foreach (ActiveOrder order in ActiveOrders) { - if (order.Second.HasValue) { order.Second -= deltaTime; } + if (order.FadeOutTime.HasValue) { order.FadeOutTime -= deltaTime; } } - ActiveOrders.RemoveAll(o => (o.Second.HasValue && o.Second <= 0.0f) || - (o.First.TargetEntity != null && o.First.TargetEntity.Removed)); + ActiveOrders.RemoveAll(o => (o.FadeOutTime.HasValue && o.FadeOutTime <= 0.0f) || + (o.Order.TargetEntity != null && o.Order.TargetEntity.Removed)); UpdateConversations(deltaTime); UpdateProjectSpecific(deltaTime); @@ -357,20 +367,20 @@ namespace Barotrauma if (player.TeamID != npc.TeamID && !player.IsIncapacitated && player.CurrentHull == npc.CurrentHull) { List availableSpeakers = new List() { npc, player }; - List dialogFlags = new List() { "OutpostNPC", "EnterOutpost" }; + List dialogFlags = new List() { "OutpostNPC".ToIdentifier(), "EnterOutpost".ToIdentifier() }; if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { - if (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false) + if (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned") { if (npc.TeamID == CharacterTeamType.None) { - dialogFlags.Remove("OutpostNPC"); - dialogFlags.Add("Bandit"); + dialogFlags.Remove("OutpostNPC".ToIdentifier()); + dialogFlags.Add("Bandit".ToIdentifier()); } else if (npc.TeamID == CharacterTeamType.FriendlyNPC) { - dialogFlags.Remove("OutpostNPC"); - dialogFlags.Add("Hostage"); + dialogFlags.Remove("OutpostNPC".ToIdentifier()); + dialogFlags.Add("Hostage".ToIdentifier()); } } else if (campaignMode.Map?.CurrentLocation?.Reputation != null) @@ -381,11 +391,11 @@ namespace Barotrauma campaignMode.Map.CurrentLocation.Reputation.Value); if (normalizedReputation < 0.2f) { - dialogFlags.Add("LowReputation"); + dialogFlags.Add("LowReputation".ToIdentifier()); } else if (normalizedReputation > 0.8f) { - dialogFlags.Add("HighReputation"); + dialogFlags.Add("HighReputation".ToIdentifier()); } } } @@ -451,13 +461,18 @@ namespace Barotrauma // Prioritize those who are on the same submarine as the controlled character .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) // Prioritize those who are already ordered to operate the device - .ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity)) + .ThenByDescending(c + => order.Category == OrderCategory.Operate + && c.CurrentOrders.Any(o + => o != null + && o.Identifier == order.Identifier + && o.TargetEntity == order.TargetEntity)) // Prioritize those with the appropriate job for the order - .ThenByDescending(c => order.HasAppropriateJob(c)) + .ThenByDescending(order.HasAppropriateJob) // Prioritize those who don't yet have the same order (which allows quick-assigning the order to different characters) - .ThenByDescending(c => c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier)) + .ThenByDescending(c => c.CurrentOrders.None(o => o != null && o.Identifier == order.Identifier)) // Prioritize those with the preferred job for the order - .ThenByDescending(c => order.HasPreferredJob(c)) + .ThenByDescending(order.HasPreferredJob) // Prioritize bots over player-controlled characters .ThenByDescending(c => c.IsBot) // Prioritize those with a lower current objective priority @@ -472,12 +487,12 @@ namespace Barotrauma { ActiveOrdersElement = new XElement("activeorders"); // Only save orders with no fade out time (e.g. ignore orders) - var ordersToSave = new List(); + var ordersToSave = new List(); foreach (var activeOrder in ActiveOrders) { - var order = activeOrder?.First; - if (order == null || activeOrder.Second.HasValue) { continue; } - ordersToSave.Add(new OrderInfo(order, null, CharacterInfo.HighestManualOrderPriority)); + var order = activeOrder?.Order; + if (order == null || activeOrder.FadeOutTime.HasValue) { continue; } + ordersToSave.Add(order.WithManualPriority(CharacterInfo.HighestManualOrderPriority)); } CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray()); parentElement?.Add(ActiveOrdersElement); @@ -489,22 +504,22 @@ namespace Barotrauma foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement)) { IIgnorable ignoreTarget = null; - if (orderInfo.Order.IsIgnoreOrder) + if (orderInfo.IsIgnoreOrder) { - switch (orderInfo.Order.TargetType) + switch (orderInfo.TargetType) { case Order.OrderTargetType.Entity: - ignoreTarget = orderInfo.Order.TargetEntity as IIgnorable; + ignoreTarget = orderInfo.TargetEntity as IIgnorable; break; - case Order.OrderTargetType.WallSection when orderInfo.Order.TargetEntity is Structure s && orderInfo.Order.WallSectionIndex.HasValue: - ignoreTarget = s.GetSection(orderInfo.Order.WallSectionIndex.Value) as IIgnorable; + case Order.OrderTargetType.WallSection when orderInfo.TargetEntity is Structure s && orderInfo.WallSectionIndex.HasValue: + ignoreTarget = s.GetSection(orderInfo.WallSectionIndex.Value); break; default: DebugConsole.ThrowError("Error loading an ignore order - can't find a proper ignore target"); continue; } } - if (orderInfo.Order.TargetEntity == null || (orderInfo.Order.IsIgnoreOrder && ignoreTarget == null)) + if (orderInfo.TargetEntity == null || (orderInfo.IsIgnoreOrder && ignoreTarget == null)) { // The order target doesn't exist anymore, just discard the loaded order continue; @@ -513,7 +528,7 @@ namespace Barotrauma { ignoreTarget.OrderedToBeIgnored = true; } - AddOrder(orderInfo.Order, null); + AddOrder(orderInfo, null); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index ba5a9912b..1d88c39a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -10,7 +10,7 @@ namespace Barotrauma { public CampaignMode Campaign { get; } - private readonly Dictionary data = new Dictionary(); + private readonly Dictionary data = new Dictionary(); public CampaignMetadata(CampaignMode campaign) { @@ -21,15 +21,15 @@ namespace Barotrauma { Campaign = campaign; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) { - string identifier = subElement.GetAttributeString("key", string.Empty).ToLowerInvariant(); + Identifier identifier = subElement.GetAttributeIdentifier("key", Identifier.Empty); string value = subElement.GetAttributeString("value", string.Empty); string valueType = subElement.GetAttributeString("type", string.Empty); - if (string.IsNullOrWhiteSpace(identifier) || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType)) + if (identifier.IsEmpty || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType)) { DebugConsole.ThrowError("Unable to load value because one or more of the required attributes are empty.\n" + $"key: \"{identifier}\", value: \"{value}\", type: \"{valueType}\""); @@ -43,28 +43,20 @@ namespace Barotrauma DebugConsole.ThrowError($"Type for {identifier} not found ({valueType})."); continue; } - - if (type == typeof(float)) + else if (type == typeof(Identifier)) { - if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatValue)) - { - DebugConsole.ThrowError($"Error in campaign metadata: could not parse \"{value}\" as a float."); - continue; - } - data.Add(identifier, floatValue); + data.Add(identifier, value.ToIdentifier()); } else { - data.Add(identifier, Convert.ChangeType(value, type)); + data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo)); } } } } - public void SetValue(string identifier, object value) + public void SetValue(Identifier identifier, object value) { - identifier = identifier.ToLowerInvariant(); - DebugConsole.Log($"Set the value \"{identifier}\" to {value}"); if (!data.ContainsKey(identifier)) @@ -76,33 +68,32 @@ namespace Barotrauma data[identifier] = value; } - public float GetFloat(string identifier, float? defaultValue = null) + public float GetFloat(Identifier identifier, float? defaultValue = null) { return (float)GetTypeOrDefault(identifier, typeof(float), defaultValue ?? 0f); } - public int GetInt(string identifier, int? defaultValue = null) + public int GetInt(Identifier identifier, int? defaultValue = null) { return (int)GetTypeOrDefault(identifier, typeof(int), defaultValue ?? 0); } - public bool GetBoolean(string identifier, bool? defaultValue = null) + public bool GetBoolean(Identifier identifier, bool? defaultValue = null) { return (bool)GetTypeOrDefault(identifier, typeof(bool), defaultValue ?? false); } - public string GetString(string identifier, string? defaultValue = null) + public string GetString(Identifier identifier, string? defaultValue = null) { return (string)GetTypeOrDefault(identifier, typeof(string), defaultValue ?? string.Empty); } - public bool HasKey(string identifier) + public bool HasKey(Identifier identifier) { - identifier = identifier.ToLowerInvariant(); return data.ContainsKey(identifier); } - private object GetTypeOrDefault(string identifier, Type type, object defaultValue) + private object GetTypeOrDefault(Identifier identifier, Type type, object defaultValue) { object? value = GetValue(identifier); @@ -122,7 +113,7 @@ namespace Barotrauma return defaultValue; } - public object? GetValue(string identifier) + public object? GetValue(Identifier identifier) { return data.ContainsKey(identifier) ? data[identifier] : null; } @@ -133,16 +124,16 @@ namespace Barotrauma foreach (var (key, value) in data) { - string valueStr = value?.ToString() ?? ""; - if (value?.GetType() == typeof(float)) + string valueStr = value.ToString() ?? throw new NullReferenceException(); + if (value is float f) { - valueStr = ((float)value).ToString("G", CultureInfo.InvariantCulture); + valueStr = f.ToString("G", CultureInfo.InvariantCulture); } element.Add(new XElement("Data", new XAttribute("key", key), new XAttribute("value", valueStr), - new XAttribute("type", value?.GetType()))); + new XAttribute("type", value.GetType()))); } #if DEBUG DebugConsole.Log(element.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index d22a8e83c..c31f37f60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -18,16 +18,14 @@ namespace Barotrauma } } - internal class FactionPrefab : IDisposable + internal class FactionPrefab : Prefab { - public static List Prefabs { get; set; } + public readonly static PrefabCollection Prefabs = new PrefabCollection(); - public string Name { get; } + public LocalizedString Name { get; } - public string Description { get; } - public string ShortDescription { get; } - - public string Identifier { get; } + public LocalizedString Description { get; } + public LocalizedString ShortDescription { get; } /// /// How low the reputation can drop on this faction @@ -52,17 +50,16 @@ namespace Barotrauma public Color IconColor { get; } #endif - private FactionPrefab(XElement element) + public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty)) { - Identifier = element.GetAttributeString("identifier", string.Empty); MinReputation = element.GetAttributeInt("minreputation", -100); MaxReputation = element.GetAttributeInt("maxreputation", 100); InitialReputation = element.GetAttributeInt("initialreputation", 0); - Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}", returnNull: true) ?? "Unnamed"; - Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description", returnNull: true) ?? ""; - ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription", returnNull: true) ?? ""; + Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed"); + Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback(""); + ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback(""); #if CLIENT - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) @@ -78,64 +75,12 @@ namespace Barotrauma #endif } - public static void LoadFactions() - { - Prefabs?.ForEach(set => set.Dispose()); - Prefabs = new List(); - IEnumerable files = GameMain.Instance.GetFilesOfType(ContentType.Factions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - XElement? rootElement = doc?.Root; - - if (doc == null || rootElement == null) { continue; } - - if (doc.Root.IsOverride()) - { - Prefabs.Clear(); - DebugConsole.NewMessage($"Overriding all factions with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in rootElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the faction config '{elementName}' in file '{file.Path}'"); - continue; - } - - var existingParams = Prefabs.Find(set => set.Identifier == identifier); - if (existingParams != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding faction config '{identifier}' using the file '{file.Path}'", Color.Yellow); - Prefabs.Remove(existingParams); - } - else - { - DebugConsole.ThrowError($"Duplicate faction config: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - - Prefabs.Add(new FactionPrefab(element)); - } - } - } - - public void Dispose() + public override void Dispose() { #if CLIENT Icon?.Remove(); Icon = null; #endif - GC.SuppressFinalize(this); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index ada24867c..820d8227e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -13,13 +13,13 @@ namespace Barotrauma public const float MinReputationLossPerStolenItem = 0.5f; public const float MaxReputationLossPerStolenItem = 10.0f; - public string Identifier { get; } + public Identifier Identifier { get; } public int MinReputation { get; } public int MaxReputation { get; } public int InitialReputation { get; } public CampaignMetadata Metadata { get; } - private readonly string metaDataIdentifier; + private readonly Identifier metaDataIdentifier; /// /// Reputation value normalized to the range of 0-1 @@ -46,8 +46,8 @@ namespace Barotrauma if (increase != 0 && Character.Controlled != null) { Character.Controlled.AddMessage( - TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name), - increase > 0 ? GUI.Style.Green : GUI.Style.Red, + TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name).Value, + increase > 0 ? GUIStyle.Green : GUIStyle.Red, playSound: true, Identifier, increase, lifetime: 5.0f); } #endif @@ -80,23 +80,23 @@ namespace Barotrauma public readonly Location Location; - public Reputation(CampaignMetadata metadata, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) + public Reputation(CampaignMetadata metadata, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation) : this(metadata, null, location, identifier, minReputation, maxReputation, initialReputation) { } public Reputation(CampaignMetadata metadata, Faction faction, int minReputation, int maxReputation, int initialReputation) - : this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}", minReputation, maxReputation, initialReputation) + : this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}".ToIdentifier(), minReputation, maxReputation, initialReputation) { } - private Reputation(CampaignMetadata metadata, Faction faction, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) + private Reputation(CampaignMetadata metadata, Faction faction, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation) { System.Diagnostics.Debug.Assert(metadata != null); System.Diagnostics.Debug.Assert(faction != null || location != null); Metadata = metadata; - Identifier = identifier.ToLowerInvariant(); - metaDataIdentifier = $"reputation.{Identifier}"; + Identifier = identifier; + metaDataIdentifier = $"reputation.{Identifier}".ToIdentifier(); MinReputation = minReputation; MaxReputation = maxReputation; InitialReputation = initialReputation; @@ -104,12 +104,12 @@ namespace Barotrauma Location = location; } - public string GetReputationName() + public LocalizedString GetReputationName() { return GetReputationName(NormalizedValue); } - public static string GetReputationName(float normalizedValue) + public static LocalizedString GetReputationName(float normalizedValue) { if (normalizedValue < HostileThreshold) { @@ -135,36 +135,36 @@ namespace Barotrauma { if (normalizedValue < HostileThreshold) { - return GUI.Style.ColorReputationVeryLow; + return GUIStyle.ColorReputationVeryLow; } else if (normalizedValue < 0.4f) { - return GUI.Style.ColorReputationLow; + return GUIStyle.ColorReputationLow; } else if (normalizedValue < 0.6f) { - return GUI.Style.ColorReputationNeutral; + return GUIStyle.ColorReputationNeutral; } else if (normalizedValue < 0.8f) { - return GUI.Style.ColorReputationHigh; + return GUIStyle.ColorReputationHigh; } - return GUI.Style.ColorReputationVeryHigh; + return GUIStyle.ColorReputationVeryHigh; } - public string GetFormattedReputationText(bool addColorTags = false) + public LocalizedString GetFormattedReputationText(bool addColorTags = false) { return GetFormattedReputationText(NormalizedValue, Value, addColorTags); } - public static string GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false) + public static LocalizedString GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false) { - string reputationName = GetReputationName(normalizedValue); - string formattedReputation = TextManager.GetWithVariables("reputationformat", - new string[] { "[reputationname]", "[reputationvalue]" }, - new string[] { reputationName, ((int)Math.Round(value)).ToString() }); + LocalizedString reputationName = GetReputationName(normalizedValue); + LocalizedString formattedReputation = TextManager.GetWithVariables("reputationformat", + ("[reputationname]", reputationName), + ("[reputationvalue]", ((int)Math.Round(value)).ToString())); if (addColorTags) { - formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖"; + formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖"+ formattedReputation+"‖end‖"; } return formattedReputation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 21e7ecf93..b384c847f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -304,11 +304,11 @@ namespace Barotrauma if (levelData.HasBeaconStation && !levelData.IsBeaconActive) { - var beaconMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))); + var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))); if (beaconMissionPrefabs.Any()) { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, beaconMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand); + var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => (float)p.Commonness, rand); if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type)) { extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); @@ -317,7 +317,7 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); if (!huntingGroundsMissionPrefabs.Any()) { DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found."); @@ -325,7 +325,7 @@ namespace Barotrauma else { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)Math.Max(p.Commonness, 0.1f)).ToList(), rand); + var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, p => (float)Math.Max(p.Commonness, 0.1f), rand); if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); @@ -692,13 +692,13 @@ namespace Barotrauma if (CampaignMetadata != null) { - int loops = CampaignMetadata.GetInt("campaign.endings", 0); - CampaignMetadata.SetValue("campaign.endings", loops + 1); + int loops = CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0); + CampaignMetadata.SetValue("campaign.endings".ToIdentifier(), loops + 1); } GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Complete, - Preset?.Identifier ?? "none"); + Preset?.Identifier.Value ?? "none"); string eventId = "FinishCampaign:"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); @@ -717,7 +717,7 @@ namespace Barotrauma location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); Money -= characterInfo.Salary; - GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier ?? "unknown"); + GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); return true; } @@ -740,8 +740,9 @@ namespace Barotrauma HumanAIController humanAI = npc.AIController as HumanAIController; if (humanAI == null) { yield return CoroutineStatus.Success; } - var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)); - humanAI.SetForcedOrder(waitOrder, string.Empty, null); + var waitOrderPrefab = OrderPrefab.Prefabs["wait"]; + var waitOrder = new Order(waitOrderPrefab, Identifier.Empty, null, orderGiver: null); + humanAI.SetForcedOrder(waitOrder); var waitObjective = humanAI.ObjectiveManager.ForcedOrder; humanAI.FaceTarget(interactor); @@ -782,7 +783,7 @@ namespace Barotrauma character.SetCustomInteract( NPCInteract, #if CLIENT - hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameMain.Config.KeyBindText(InputType.Use))); + hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else hudText: TextManager.Get("CampaignInteraction." + interactionType)); #endif @@ -855,8 +856,8 @@ namespace Barotrauma { GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create( - TextManager.Get("RadioAnnouncerName"), - TextManager.Get("TooFarFromOutpostWarning"), Networking.ChatMessageType.Default, null), c); + TextManager.Get("RadioAnnouncerName").Value, + TextManager.Get("TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default, null), c); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs index 1061e9754..fc2c6652e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs @@ -27,7 +27,7 @@ namespace Barotrauma get { return preset.IsSinglePlayer; } } - public string Name + public LocalizedString Name { get { return preset.Name; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index 608dd2b89..f28cb563c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -19,20 +19,20 @@ namespace Barotrauma public readonly Type GameModeType; - public readonly string Name; - public readonly string Description; + public readonly LocalizedString Name; + public readonly LocalizedString Description; - public readonly string Identifier; + public readonly Identifier Identifier; public readonly bool IsSinglePlayer; //are clients allowed to vote for this gamemode public readonly bool Votable; - public GameModePreset(string identifier, Type type, bool isSinglePlayer = false, bool votable = true) + public GameModePreset(Identifier identifier, Type type, bool isSinglePlayer = false, bool votable = true) { Name = TextManager.Get("GameMode." + identifier); - Description = TextManager.Get("GameModeDescription." + identifier, returnNull: true) ?? ""; + Description = TextManager.Get("GameModeDescription." + identifier).Fallback(""); Identifier = identifier; GameModeType = type; @@ -46,15 +46,15 @@ namespace Barotrauma public static void Init() { #if CLIENT - Tutorial = new GameModePreset("tutorial", typeof(TutorialMode), true); - DevSandbox = new GameModePreset("devsandbox", typeof(GameMode), true); - SinglePlayerCampaign = new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true); - TestMode = new GameModePreset("testmode", typeof(TestGameMode), true); + Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true); + DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true); + SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true); + TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true); #endif - Sandbox = new GameModePreset("sandbox", typeof(GameMode), false); - Mission = new GameModePreset("mission", typeof(CoOpMode), false); - PvP = new GameModePreset("pvp", typeof(PvPMode), false); - MultiPlayerCampaign = new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false); + Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false); + Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false); + PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false); + MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 533a3773e..ab5ea60ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -120,7 +120,7 @@ namespace Barotrauma #endif } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -192,7 +192,7 @@ namespace Barotrauma { var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath); if (characterDataDoc?.Root == null) { return; } - foreach (XElement subElement in characterDataDoc.Root.Elements()) + foreach (var subElement in characterDataDoc.Root.Elements()) { characterData.Add(new CharacterCampaignData(subElement)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 817e4ff80..d9188c9b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1,8 +1,11 @@ -using Barotrauma.IO; +#nullable enable + +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -14,11 +17,11 @@ namespace Barotrauma public readonly EventManager EventManager; - public GameMode GameMode; + public GameMode? GameMode; //two locations used as the start and end in the MP mode - private Location[] dummyLocations; - public CrewManager CrewManager; + private Location[]? dummyLocations; + public CrewManager? CrewManager; public double RoundStartTime; @@ -30,15 +33,15 @@ namespace Barotrauma public CharacterTeamType? WinningTeam; public bool IsRunning { get; private set; } - + public bool RoundEnding { get; private set; } - public Level Level { get; private set; } - public LevelData LevelData { get; private set; } + public Level? Level { get; private set; } + public LevelData? LevelData { get; private set; } public bool MirrorLevel { get; private set; } - public Map Map + public Map? Map { get { @@ -46,7 +49,7 @@ namespace Barotrauma } } - public CampaignMode Campaign + public CampaignMode? Campaign { get { @@ -61,6 +64,7 @@ namespace Barotrauma { if (Map != null) { return Map.CurrentLocation; } if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } } @@ -71,6 +75,7 @@ namespace Barotrauma { if (Map != null) { return Map.SelectedLocation; } if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } } @@ -79,13 +84,13 @@ namespace Barotrauma public List OwnedSubmarines = new List(); - public Submarine Submarine { get; set; } + public Submarine? Submarine { get; set; } - public string SavePath { get; set; } + public string? SavePath { get; set; } partial void InitProjSpecific(); - private GameSession(SubmarineInfo submarineInfo, List ownedSubmarines = null) + private GameSession(SubmarineInfo submarineInfo, List? ownedSubmarines = null) { InitProjSpecific(); SubmarineInfo = submarineInfo; @@ -109,21 +114,21 @@ namespace Barotrauma /// /// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved). /// - public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string seed = null, MissionType missionType = MissionType.None) + public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string? seed = null, MissionType missionType = MissionType.None) : this(submarineInfo) { this.SavePath = savePath; - CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); + CrewManager = new CrewManager(gameModePreset.IsSinglePlayer); GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType); } /// /// Start a new GameSession with a specific pre-selected mission. /// - public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, IEnumerable missionPrefabs = null) + public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string? seed = null, IEnumerable? missionPrefabs = null) : this(submarineInfo) { - CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); + CrewManager = new CrewManager(gameModePreset.IsSinglePlayer); GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs); } @@ -134,9 +139,10 @@ namespace Barotrauma { this.SavePath = saveFile; GameMain.GameSession = this; + XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); //selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); - foreach (XElement subElement in doc.Root.Elements()) + foreach (var subElement in rootElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -154,7 +160,7 @@ namespace Barotrauma var mpCampaign = MultiPlayerCampaign.LoadNew(subElement); GameMode = mpCampaign; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { + { mpCampaign.LoadNewLevel(); //save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance SaveUtil.SaveGame(saveFile); @@ -164,7 +170,7 @@ namespace Barotrauma } } - private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable missionPrefabs = null, MissionType missionType = MissionType.None) + private GameMode InstantiateGameMode(GameModePreset gameModePreset, string? seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable? missionPrefabs = null, MissionType missionType = MissionType.None) { if (gameModePreset.GameModeType == typeof(CoOpMode) || gameModePreset.GameModeType == typeof(PvPMode)) { @@ -193,7 +199,7 @@ namespace Barotrauma else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); - if (campaign != null && selectedSub != null) + if (selectedSub != null) { campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); } @@ -203,7 +209,7 @@ namespace Barotrauma else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign)) { var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); - if (campaign != null && selectedSub != null) + if (selectedSub != null) { campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); } @@ -277,10 +283,9 @@ namespace Barotrauma } } - Campaign.Money -= cost; + Campaign!.Money -= cost; GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); - ((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine; return newSubmarine; } @@ -317,9 +322,10 @@ namespace Barotrauma return isRadiated; } - public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams levelGenerationParams = null) + public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams? levelGenerationParams = null) { - LevelData randomLevel = null; + if (GameMode == null) { return; } + LevelData? randomLevel = null; foreach (Mission mission in Missions.Union(GameMode.Missions)) { MissionPrefab missionPrefab = mission.Prefab; @@ -327,7 +333,7 @@ namespace Barotrauma missionPrefab.AllowedLocationTypes.Any() && !missionPrefab.AllowedConnectionTypes.Any()) { - LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase))); + LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)); CreateDummyLocations(locationType); randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; @@ -337,8 +343,10 @@ namespace Barotrauma StartRound(randomLevel); } - public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) + public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null) { + AfflictionPrefab.LoadAllEffects(); + MirrorLevel = mirrorLevel; if (SubmarineInfo == null) { @@ -375,7 +383,7 @@ namespace Barotrauma } } - foreach (Mission mission in GameMode.Missions) + foreach (Mission mission in GameMode!.Missions) { // setting level for missions that may involve difficulty-related submarine creation mission.SetLevel(levelData); @@ -403,7 +411,10 @@ namespace Barotrauma } } - Level level = null; + //Clear out the stored grids + Powered.Grids.Clear(); + + Level? level = null; if (levelData != null) { level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost); @@ -413,11 +424,11 @@ namespace Barotrauma GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Start, - GameMode?.Preset?.Identifier ?? "none"); + GameMode?.Preset?.Identifier.Value ?? "none"); - string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; + string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); foreach (Mission mission in missions) { @@ -425,12 +436,12 @@ namespace Barotrauma } if (Level.Loaded != null) { - string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : - Level.Loaded.GenerationParams?.Identifier; - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + (levelId ?? "null")); + Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId); } - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none")); #if CLIENT if (GameMode is TutorialMode tutorialMode) { @@ -452,16 +463,16 @@ namespace Barotrauma { GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled"); } - bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData.Biome); + bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome); if (firstTimeInBiome) { - GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime); - GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels); + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime); + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels); } } #if CLIENT - if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } + if (GameMode is CampaignMode && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary; if (existingRoundSummary?.ContinueButton != null) @@ -474,7 +485,7 @@ namespace Barotrauma if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) { GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false); - if (EndLocation != null) + if (EndLocation != null && levelData != null) { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); @@ -503,7 +514,7 @@ namespace Barotrauma #endif } - private void InitializeLevel(Level level) + private void InitializeLevel(Level? level) { //make sure no status effects have been carried on from the next round //(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully) @@ -514,7 +525,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null; #endif if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } - if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameMain.Config.LosMode; } + if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } #endif LevelData = level?.LevelData; Level = level; @@ -531,28 +542,28 @@ namespace Barotrauma Entity.Spawner = new EntitySpawner(); - missions.Clear(); - GameMode.AddExtraMissions(LevelData); - missions.AddRange(GameMode.Missions); - GameMode.Start(); - foreach (Mission mission in missions) + if (GameMode != null && Submarine != null) { - int prevEntityCount = Entity.GetEntities().Count(); - mission.Start(Level.Loaded); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) + missions.Clear(); + GameMode.AddExtraMissions(LevelData); + missions.AddRange(GameMode.Missions); + GameMode.Start(); + foreach (Mission mission in missions) { - DebugConsole.ThrowError( - $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + - "The clients should not instantiate entities themselves when starting the mission," + - " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial."); + int prevEntityCount = Entity.GetEntities().Count(); + mission.Start(Level.Loaded); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) + { + DebugConsole.ThrowError( + $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + + "The clients should not instantiate entities themselves when starting the mission," + + " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial."); + } } - } - EventManager?.StartRound(Level.Loaded); - SteamAchievementManager.OnStartRound(); + EventManager?.StartRound(Level.Loaded); + SteamAchievementManager.OnStartRound(); - if (GameMode != null) - { GameMode.ShowStartMessage(); if (GameMain.NetworkMember == null) @@ -571,7 +582,7 @@ namespace Barotrauma } } - GameMain.Config.RecentlyEncounteredCreatures.Clear(); + CreatureMetrics.Instance.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundStartTime = Timing.TotalTime; @@ -579,11 +590,11 @@ namespace Barotrauma IsRunning = true; } - public void PlaceSubAtStart(Level level) + public void PlaceSubAtStart(Level? level) { - if (level == null) + if (level == null || Submarine == null) { - Submarine.MainSub.SetPosition(Vector2.Zero); + Submarine?.SetPosition(Vector2.Zero); return; } @@ -601,7 +612,7 @@ namespace Barotrauma //find the port that's the nearest to the outpost and dock if one is found float closestDistance = 0.0f; - DockingPort myPort = null, outPostPort = null; + DockingPort? myPort = null, outPostPort = null; foreach (DockingPort port in DockingPort.List) { if (port.IsHorizontal || port.Docked) { continue; } @@ -687,7 +698,7 @@ namespace Barotrauma UpdateProjSpecific(deltaTime); } - public Mission GetMission(int index) + public Mission? GetMission(int index) { if (index < 0 || index >= missions.Count) { return null; } return missions[index]; @@ -698,12 +709,13 @@ namespace Barotrauma return missions.IndexOf(mission); } - public void EnforceMissionOrder(List missionIdentifiers) + public void EnforceMissionOrder(List missionIdentifiers) { List sortedMissions = new List(); - foreach (string missionId in missionIdentifiers) + foreach (Identifier missionId in missionIdentifiers) { var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId); + if (matchingMission == null) { continue; } sortedMissions.Add(matchingMission); missions.Remove(matchingMission); } @@ -711,21 +723,24 @@ namespace Barotrauma } partial void UpdateProjSpecific(float deltaTime); - + public static IEnumerable GetSessionCrewCharacters() { #if SERVER return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); #else - if (GameMain.GameSession == null) { return Enumerable.Empty(); } + if (GameMain.GameSession?.CrewManager is null) { return Enumerable.Empty(); } return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null && !c.IsDead); -#endif +#endif } - public void EndRound(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public void EndRound(string endMessage, List? traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { RoundEnding = true; + //Clear the grids to allow for garbage collection + Powered.Grids.Clear(); + try { IEnumerable crewCharacters = GetSessionCrewCharacters(); @@ -744,7 +759,7 @@ namespace Barotrauma if (missions.Any()) { - if (missions.Any(m => m.Completed)) + if (missions.Any()) { foreach (Character character in crewCharacters) { @@ -786,19 +801,20 @@ namespace Barotrauma GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); + AfflictionPrefab.ClearAllEffects(); IsRunning = false; #if CLIENT - bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); + bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead); #else bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); #endif double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, - GameMode?.Name ?? "none", + GameMode?.Name?.Value ?? "none", roundDuration); - string eventId = "EndRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; + string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { @@ -820,7 +836,7 @@ namespace Barotrauma { double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), roundDuration); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); foreach (Mission mission in missions) { @@ -828,11 +844,11 @@ namespace Barotrauma } if (Level.Loaded != null) { - string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : - Level.Loaded.GenerationParams?.Identifier; - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + (levelId ?? "null")), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration); + Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), roundDuration); } if (Submarine.MainSub != null) @@ -874,7 +890,7 @@ namespace Barotrauma { characterType = "Player"; } - GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value); + GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier.Value ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value); } } #if CLIENT @@ -895,18 +911,18 @@ namespace Barotrauma public void KillCharacter(Character character) { #if CLIENT - CrewManager.KillCharacter(character); + CrewManager?.KillCharacter(character); #endif } public void ReviveCharacter(Character character) { #if CLIENT - CrewManager.ReviveCharacter(character); + CrewManager?.ReviveCharacter(character); #endif } - public static bool IsCompatibleWithEnabledContentPackages(IList contentPackagePaths, out string errorMsg) + public static bool IsCompatibleWithEnabledContentPackages(IList contentPackagePaths, out LocalizedString errorMsg) { errorMsg = ""; //no known content packages, must be an older save file @@ -915,15 +931,15 @@ namespace Barotrauma List missingPackages = new List(); foreach (string packagePath in contentPackagePaths) { - if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.Path == packagePath)) + if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.Path == packagePath)) { missingPackages.Add(packagePath); } } List excessPackages = new List(); - foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) + foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All) { - if (!cp.HasMultiplayerIncompatibleContent) { continue; } + //if (!cp.HasMultiplayerIncompatibleContent) { continue; } if (!contentPackagePaths.Any(p => p == cp.Path)) { excessPackages.Add(cp.Name); @@ -933,8 +949,8 @@ namespace Barotrauma bool orderMismatch = false; if (missingPackages.Count == 0 && missingPackages.Count == 0) { - var enabledPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); - for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Count; i++) + var enabledPackages = ContentPackageManager.EnabledPackages.All/*.Where(cp => cp.HasMultiplayerIncompatibleContent)*/.ToImmutableArray(); + for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Length; i++) { if (contentPackagePaths[i] != enabledPackages[i].Path) { @@ -956,17 +972,17 @@ namespace Barotrauma } if (excessPackages.Count == 1) { - if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; } + if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; } errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackage", "[incompatiblecontentpackage]", excessPackages[0]); } else if (excessPackages.Count > 1) { - if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; } + if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; } errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackages", "[incompatiblecontentpackages]", string.Join(", ", excessPackages)); } if (orderMismatch) { - if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; } + if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; } errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackagePaths)); } @@ -981,24 +997,25 @@ namespace Barotrauma } XDocument doc = new XDocument(new XElement("Gamesession")); + XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); - doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); - doc.Root.Add(new XAttribute("version", GameMain.Version)); + rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); + rootElement.Add(new XAttribute("version", GameMain.Version)); var submarineInfo = Campaign?.PendingSubmarineSwitch ?? SubmarineInfo; - doc.Root.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name)); + rootElement.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name)); if (OwnedSubmarines != null) { List ownedSubmarineNames = new List(); var ownedSubsElement = new XElement("ownedsubmarines"); - doc.Root.Add(ownedSubsElement); + rootElement.Add(ownedSubsElement); foreach (var ownedSub in OwnedSubmarines) { ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name))); } } - doc.Root.Add(new XAttribute("mapseed", Map.Seed)); - doc.Root.Add(new XAttribute("selectedcontentpackages", - string.Join("|", GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); + if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); } + rootElement.Add(new XAttribute("selectedcontentpackages", + string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); ((CampaignMode)GameMode).Save(doc.Root); @@ -1007,7 +1024,7 @@ namespace Barotrauma /*public void Load(XElement saveElement) { - foreach (XElement subElement in saveElement.Elements()) + foreach (var subElement in saveElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 59b6e4bc5..43baec7c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -29,8 +29,8 @@ namespace Barotrauma JobPrefab job = location.Type.GetRandomHireable(); if (job == null) { return; } - var variant = Rand.Range(0, job.Variants, Rand.RandSync.Server); - AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant)); + var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient); + AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 80fe1bb35..235b157d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -63,7 +63,7 @@ namespace Barotrauma public struct NetAffliction : INetSerializableStruct { [NetworkSerialize] - public string Identifier; + public Identifier Identifier; [NetworkSerialize] public ushort Strength; @@ -116,7 +116,7 @@ namespace Barotrauma foreach (AfflictionPrefab prefab in AfflictionPrefab.List) { - if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)) + if (prefab.Identifier == Identifier) { cachedPrefab = prefab; return prefab; @@ -128,7 +128,7 @@ namespace Barotrauma set { cachedPrefab = value; - Identifier = value?.Identifier ?? string.Empty; + Identifier = value?.Identifier ?? Identifier.Empty; Strength = 0; Price = 0; } @@ -136,12 +136,12 @@ namespace Barotrauma public readonly bool AfflictionEquals(AfflictionPrefab prefab) { - return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + return prefab.Identifier == Identifier; } public readonly bool AfflictionEquals(NetAffliction affliction) { - return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + return affliction.Identifier == Identifier; } } @@ -221,7 +221,7 @@ namespace Barotrauma foreach (NetAffliction affliction in crewMember.Afflictions) { - health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength); + health.ReduceAfflictionOnAllLimbs(affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength); } } @@ -333,21 +333,21 @@ namespace Barotrauma #if DEBUG && CLIENT private static readonly CharacterInfo[] TestInfos = { - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human") + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName) }; private static readonly NetAffliction[] TestAfflictions = { - new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 }, - new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 }, - new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 }, - new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 } + new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 }, + new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 }, + new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 }, + new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 } }; #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 5c0429327..aede268f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma @@ -96,6 +97,8 @@ namespace Barotrauma public UpgradeManager(CampaignMode campaign) { + UpgradeCategory.Categories.ForEach(c => c.DeterminePrefabsThatAllowUpgrades()); + DebugConsole.Log("Created brand new upgrade manager."); Campaign = campaign; } @@ -112,7 +115,7 @@ namespace Barotrauma } else { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -158,7 +161,7 @@ namespace Barotrauma price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); } //refund the current item - if (replacement != item.prefab) + if (replacement != ((MapEntity)item).Prefab) { price -= item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); } @@ -219,18 +222,18 @@ namespace Barotrauma // only make the NPC speak if more than 5 minutes have passed since the last purchased service if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) { - UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer); lastUpgradeSpeak = DateTime.Now; } } Campaign.Money -= price; - GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier); + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value); PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); #if CLIENT - DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUI.Style.Orange); + DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUIStyle.Orange); #endif if (upgrade == null) @@ -285,7 +288,7 @@ namespace Barotrauma return; } - if (itemToRemove.prefab == itemToInstall) + if (((MapEntity)itemToRemove).Prefab == itemToInstall) { DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!)."); return; @@ -318,13 +321,13 @@ namespace Barotrauma // only make the NPC speak if more than 5 minutes have passed since the last purchased service if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) { - UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer); lastUpgradeSpeak = DateTime.Now; } } Campaign.Money -= price; - GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier); + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier.Value); foreach (Item itemToSwap in linkedItems) { @@ -367,7 +370,7 @@ namespace Barotrauma return; } - if (itemToRemove?.PendingItemSwap == null && string.IsNullOrEmpty(itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall)) + if (itemToRemove?.PendingItemSwap == null && (itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true)) { DebugConsole.ThrowError($"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured)."); return; @@ -385,7 +388,7 @@ namespace Barotrauma // only make the NPC speak if more than 5 minutes have passed since the last purchased service if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) { - UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer); lastUpgradeSpeak = DateTime.Now; } } @@ -429,7 +432,7 @@ namespace Barotrauma { if (!(secondLinkedEntity is Item linkedItem) || linkedItem == item) { continue; } if (linkedItem.AllowSwapping && - linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == linkedItem.prefab.Identifier) && + linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == ((MapEntity)linkedItem).Prefab.Identifier) && linkedItem.Prefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase)) { linkedItems.Add(linkedItem); @@ -543,7 +546,7 @@ namespace Barotrauma if (upgrade == null || upgrade.Level != level || isOverMax) { - DebugLog($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); + DebugLog($"{((MapEntity)wall).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); FixUpgradeOnItem(wall, prefab, level); } } @@ -572,7 +575,7 @@ namespace Barotrauma if (upgrade == null || upgrade.Level != level || isOverMax) { - DebugLog($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing..."); + DebugLog($"{((MapEntity)item).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing..."); FixUpgradeOnItem(item, prefab, level); } } @@ -598,7 +601,7 @@ namespace Barotrauma /// /// /// New level that was applied, -1 if no upgrades were applied. - private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine parentSub = null) + private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine? parentSub = null) { int? newLevel = null; if (category.IsWallUpgrade) @@ -753,13 +756,13 @@ namespace Barotrauma // ReSharper disable once LoopCanBeConvertedToQuery foreach (XElement upgrade in element.Elements()) { - string? categoryIdentifier = upgrade.GetAttributeString("category", null); + Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty); UpgradeCategory? category = UpgradeCategory.Find(categoryIdentifier); - if (string.IsNullOrWhiteSpace(categoryIdentifier) || category == null) { continue; } + if (categoryIdentifier.IsEmpty || category == null) { continue; } - string? prefabIdentifier = upgrade.GetAttributeString("prefab", null); + Identifier prefabIdentifier = upgrade.GetAttributeIdentifier("prefab", Identifier.Empty); UpgradePrefab? prefab = UpgradePrefab.Find(prefabIdentifier); - if (string.IsNullOrWhiteSpace(prefabIdentifier) || prefab == null) { continue; } + if (prefabIdentifier.IsEmpty || prefab == null) { continue; } int level = upgrade.GetAttributeInt("level", -1); if (level < 0) { continue; } @@ -814,6 +817,6 @@ namespace Barotrauma private PurchasedUpgrade? FindMatchingUpgrade(UpgradePrefab prefab, UpgradeCategory category) => PendingUpgrades.Find(u => u.Prefab == prefab && u.Category == category); - private static string FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}"; + private static Identifier FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}".ToIdentifier(); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs deleted file mode 100644 index c8b5cd5e7..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ /dev/null @@ -1,1516 +0,0 @@ -using System.Linq; -using System.Xml.Linq; -using System.Collections.Generic; -using Microsoft.Xna.Framework; -using Barotrauma.IO; -using Barotrauma.Extensions; -using System.Diagnostics; -#if CLIENT -using Microsoft.Xna.Framework.Input; -using Microsoft.Xna.Framework.Graphics; -using Barotrauma.Tutorials; -#endif -using System; - -namespace Barotrauma -{ - public enum WindowMode - { - Windowed, Fullscreen, BorderlessWindowed - } - - public enum LosMode - { - None, - Transparent, - Opaque - } - - public partial class GameSettings - { - public const string SavePath = "config.xml"; - public const string PlayerSavePath = "config_player.xml"; - public const string VanillaContentPackagePath = "Data/ContentPackages/Vanilla"; - - public int GraphicsWidth { get; set; } - public int GraphicsHeight { get; set; } - - public bool VSyncEnabled { get; set; } - - public bool TextureCompressionEnabled { get; set; } - - public bool EnableSplashScreen { get; set; } - - public int ParticleLimit { get; set; } - - public float LightMapScale { get; set; } - public bool ChromaticAberrationEnabled { get; set; } - - public bool PauseOnFocusLost { get; set; } - public bool MuteOnFocusLost { get; set; } - public bool DynamicRangeCompressionEnabled { get; set; } - public bool VoipAttenuationEnabled { get; set; } - public bool UseDirectionalVoiceChat { get; set; } - public bool DisableVoiceChatFilters { get; set; } - - public IList AudioDeviceNames; - public IList CaptureDeviceNames; - - public string AudioOutputDevice { get; set; } - - public enum VoiceMode - { - Disabled, - PushToTalk, - Activity - }; - - public VoiceMode VoiceSetting { get; set; } - public string VoiceCaptureDevice { get; set; } - - public float NoiseGateThreshold { get; set; } - - public bool UseLocalVoiceByDefault { get; set; } - -#if CLIENT - public KeyOrMouse[] keyMapping; - private KeyOrMouse[] inventoryKeyMapping; - public static Dictionary ConsoleKeybinds = new Dictionary(); -#endif - - private WindowMode windowMode; - - private LosMode losMode; - - public List> jobPreferences; - - public string QuickStartSubmarineName; - -#if USE_STEAM - public bool RequireSteamAuthentication { get; set; } - public bool UseSteamMatchmaking { get; set; } -#else - public bool RequireSteamAuthentication - { - get { return false; } - set { /*do nothing*/ } - } - public bool UseSteamMatchmaking - { - get { return false; } - set { /*do nothing*/ } - } -#endif - public bool UseDualModeSockets { get; set; } = true; - - public bool AutoUpdateWorkshopItems; - - public WindowMode WindowMode - { - get { return windowMode; } - set - { -#if (OSX) - // Fullscreen doesn't work on macOS, so just force any usage of it to borderless windowed. - if (value == WindowMode.Fullscreen) - { - windowMode = WindowMode.BorderlessWindowed; - return; - } -#endif - windowMode = value; - } - } - - public List> JobPreferences - { - get { return jobPreferences; } - set { jobPreferences = value; } - } - - public CharacterTeamType TeamPreference { get; set; } - - public bool AreJobPreferencesEqual(List> compareTo) - { - if (jobPreferences == null || compareTo == null) return false; - if (jobPreferences.Count != compareTo.Count) return false; - - for (int i = 0; i < jobPreferences.Count; i++) - { - if (jobPreferences[i].First != compareTo[i].First || jobPreferences[i].Second != compareTo[i].Second) return false; - } - - return true; - } - - internal CharacterInfo.HeadInfo PlayerCharacterCustomization { get; set; } - - private float aimAssistAmount; - public float AimAssistAmount - { - get { return aimAssistAmount; } - set { aimAssistAmount = MathHelper.Clamp(value, 0.0f, 5.0f); } - } - - public bool EnableMouseLook { get; set; } = true; - - public bool EnableRadialDistortion { get; set; } = true; - - public bool CrewMenuOpen { get; set; } = true; - public bool ChatOpen { get; set; } = true; - - public float CorpseDespawnDelay { get; set; } = 10.0f * 60.0f; - - /// - /// How many corpses there can be in a sub before they start to get despawned - /// - public int CorpsesPerSubDespawnThreshold { get; set; } = 10; - - private string overrideSaveFolder, overrideMultiplayerSaveFolder; - - private bool unsavedSettings; - public bool UnsavedSettings - { - get - { - return unsavedSettings; - } - private set - { - unsavedSettings = value; -#if CLIENT - if (applyButton != null) - { - applyButton.Enabled = unsavedSettings; - applyButton.Text = TextManager.Get(unsavedSettings ? "ApplySettingsButtonUnsavedChanges" : "ApplySettingsButton"); - } -#endif - } - } - - private float soundVolume, musicVolume, voiceChatVolume, microphoneVolume; - - public float SoundVolume - { - get { return soundVolume; } - set - { - soundVolume = MathHelper.Clamp(value, 0.0f, 1.0f); -#if CLIENT - if (GameMain.SoundManager != null) - { - GameMain.SoundManager.SetCategoryGainMultiplier("default", soundVolume, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("ui", soundVolume, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", soundVolume, 0); - } -#endif - } - } - - public float MusicVolume - { - get { return musicVolume; } - set - { - musicVolume = MathHelper.Clamp(value, 0.0f, 1.0f); -#if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume * 0.7f, 0); -#endif - } - } - - public float VoiceChatVolume - { - get { return voiceChatVolume; } - set - { - voiceChatVolume = MathHelper.Clamp(value, 0.0f, 2.0f); -#if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("voip", Math.Min(voiceChatVolume, 1.0f), 0); -#endif - } - } - - - public int VoiceChatCutoffPrevention - { - get; - set; - } - - public const float MaxMicrophoneVolume = 10.0f; - public float MicrophoneVolume - { - get { return microphoneVolume; } - set - { - microphoneVolume = MathHelper.Clamp(value, 0.2f, MaxMicrophoneVolume); - } - } - public string Language - { - get { return TextManager.Language; } - set { TextManager.Language = value; } - } - - public ContentPackage CurrentCorePackage { get; private set; } - private readonly List enabledRegularPackages = new List(); - public IReadOnlyList EnabledRegularPackages - { - get { return enabledRegularPackages; } - } - - public IEnumerable AllEnabledPackages - { - get - { - yield return CurrentCorePackage; - foreach (var package in EnabledRegularPackages) - { - yield return package; - } - } - } - - public bool ContentPackageSelectionDirtyNotification - { - get; - set; - } - - public bool ContentPackageSelectionDirty - { - get; - private set; - } - - public XElement ServerFilterElement - { - get; - private set; - } - - public volatile bool SuppressModFolderWatcher; - - public volatile bool WaitingForAutoUpdate; - - public bool DisableInGameHints { get; set; } - -#if DEBUG - public bool AutomaticQuickStartEnabled { get; set; } - public bool AutomaticCampaignLoadEnabled { get; set; } - public bool TextManagerDebugModeEnabled { get; set; } - public bool TestScreenEnabled { get; set; } - - public bool ModBreakerMode { get; set; } -#endif - - private static int ContentFileLoadOrder(ContentFile a) - { - switch (a.Type) - { - case ContentType.Text: - return -2; - case ContentType.Afflictions: - return -1; - case ContentType.ItemAssembly: - return 1; - default: - return 0; - } - } - - public void SelectCorePackage(ContentPackage contentPackage, bool forceReloadAll = false) - { - if (!contentPackage.IsCorePackage) { return; } - if (!contentPackage.ContainsRequiredCorePackageFiles(out _)) { return; } - - ContentPackage prevCorePackage = CurrentCorePackage; - - CurrentCorePackage = contentPackage; - - if (prevCorePackage != null) - { - List filesToRemove = prevCorePackage.Files.Where(f1 => forceReloadAll || - !contentPackage.Files.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - List filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll || - !prevCorePackage.Files.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - DisableContentPackageItems(filesToRemove); - EnableContentPackageItems(filesToAdd); - - RefreshContentPackageItems(filesToAdd.Concat(filesToRemove)); - } - else - { - EnableContentPackageItems(contentPackage.Files); - RefreshContentPackageItems(contentPackage.Files); - } - } - - public void AutoSelectCorePackage(IEnumerable toRemove) - { - SelectCorePackage(ContentPackage.CorePackages.Find(cpp => - (toRemove == null || !toRemove.Contains(cpp)) && - cpp.ContainsRequiredCorePackageFiles(out _))); - } - - private List> backupModOrder; - - public void BackUpModOrder() - { - backupModOrder = new List> - { - new Tuple(CurrentCorePackage, true) - }; - for (int i = 0; i < ContentPackage.RegularPackages.Count; i++) - { - var p = ContentPackage.RegularPackages[i]; - backupModOrder.Add(new Tuple(p, EnabledRegularPackages.Contains(p))); - } - } - - public void SwapPackages(ContentPackage corePackage, List regularPackages) - { - List packagesToDisable = new List(); - packagesToDisable.Add(CurrentCorePackage); - packagesToDisable.AddRange(enabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent)); - List packagesToEnable = new List(); - packagesToEnable.Add(corePackage); - List regularPackagesToAdd = regularPackages.Where(p => p.HasMultiplayerIncompatibleContent).ToList(); - packagesToEnable.AddRange(regularPackagesToAdd); - - IEnumerable filesOfDisabledPkgs = packagesToDisable.SelectMany(p => p.Files); - IEnumerable filesOfEnabledPkgs = packagesToEnable.SelectMany(p => p.Files); - - List filesToDisable = filesOfDisabledPkgs.Where(f1 => - !filesOfEnabledPkgs.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - List filesToEnable = filesOfEnabledPkgs.Where(f1 => - !filesOfDisabledPkgs.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - CurrentCorePackage = corePackage; - enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackagesToAdd); - - DisableContentPackageItems(filesToDisable); - EnableContentPackageItems(filesToEnable); - - RefreshContentPackageItems(filesOfEnabledPkgs.Concat(filesToDisable)); - - ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p), config: this); - -#if DEBUG - Debug.Assert(enabledRegularPackages.Count == enabledRegularPackages.Distinct().Count()); -#endif - } - - public void RestoreBackupPackages() - { - if (backupModOrder == null) { return; } - - SwapPackages( - backupModOrder[0].Item1, - backupModOrder.Skip(1).Where(p => p.Item2).Select(p => p.Item1).ToList()); - ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p), config: this); - - backupModOrder = null; - } - - public void EnableRegularPackage(ContentPackage contentPackage) - { - if (contentPackage.IsCorePackage) { return; } - if (!enabledRegularPackages.Contains(contentPackage)) - { - enabledRegularPackages.Add(contentPackage); - SortContentPackages(); - - EnableContentPackageItems(contentPackage.Files); - RefreshContentPackageItems(contentPackage.Files); - } - } - - public void DisableRegularPackage(ContentPackage contentPackage) - { - if (contentPackage.IsCorePackage) { return; } - if (enabledRegularPackages.Contains(contentPackage)) - { - enabledRegularPackages.Remove(contentPackage); - SortContentPackages(); - - DisableContentPackageItems(contentPackage.Files); - RefreshContentPackageItems(contentPackage.Files); - } - } - - public void SortContentPackages(bool refreshAll = false) - { - var previousEnabledRegularPackages = enabledRegularPackages.ToList(); - - for (int i = enabledRegularPackages.Count - 1; i >= 0; i--) - { - var package = enabledRegularPackages[i]; - if (!ContentPackage.RegularPackages.Contains(package)) - { - ContentPackage replacement = ContentPackage.RegularPackages.Find(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); - if (replacement != null) - { - enabledRegularPackages[i] = replacement; - } - else - { - DisableRegularPackage(package); - } - } - } - - if (CurrentCorePackage == null) - { - AutoSelectCorePackage(null); - } - else if (!ContentPackage.CorePackages.Contains(CurrentCorePackage)) - { - ContentPackage replacement = ContentPackage.CorePackages.Find(p => p.Name.Equals(CurrentCorePackage.Name, StringComparison.OrdinalIgnoreCase)); - if (replacement != null) - { - SelectCorePackage(replacement); - } - else - { - AutoSelectCorePackage(null); - } - } - - var sortedSelected = enabledRegularPackages - .OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p)) - .ToList(); - if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) - { - CheckModded(); - return; - } - enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected); - - CharacterPrefab.Prefabs.SortAll(); - AfflictionPrefab.Prefabs.SortAll(); - JobPrefab.Prefabs.SortAll(); - ItemPrefab.Prefabs.SortAll(); - CoreEntityPrefab.Prefabs.SortAll(); - ItemAssemblyPrefab.Prefabs.SortAll(); - StructurePrefab.Prefabs.SortAll(); - -#if CLIENT - GameMain.DecalManager?.Prefabs.SortAll(); - GameMain.ParticleManager?.Prefabs.SortAll(); -#endif - - if (refreshAll) - { - RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files)); - } - - CheckModded(); - - void CheckModded() - { - if (AllEnabledPackages.Any(p => p != GameMain.VanillaContent && p.HasMultiplayerIncompatibleContent)) - { - GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Modded); - } - else - { - GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Vanilla); - } - } - } - - public void EnableContentPackageItems(IEnumerable unorderedFiles) - { - if (WaitingForAutoUpdate) { return; } - IOrderedEnumerable files = unorderedFiles.OrderBy(ContentFileLoadOrder); - foreach (ContentFile file in files) - { - switch (file.Type) - { - case ContentType.Character: - CharacterPrefab.LoadFromFile(file); - break; - case ContentType.Corpses: - CorpsePrefab.LoadFromFile(file); - break; - case ContentType.NPCConversations: - NPCConversation.LoadFromFile(file); - break; - case ContentType.Jobs: - JobPrefab.LoadFromFile(file); - break; - case ContentType.Item: - ItemPrefab.LoadFromFile(file); - break; - case ContentType.ItemAssembly: - new ItemAssemblyPrefab(file.Path); - break; - case ContentType.Structure: - StructurePrefab.LoadFromFile(file); - break; - case ContentType.Text: - TextManager.LoadTextPack(file.Path); - break; - case ContentType.Talents: - TalentPrefab.LoadFromFile(file); - break; - case ContentType.TalentTrees: - TalentTree.LoadFromFile(file); - break; -#if CLIENT - case ContentType.Particles: - GameMain.ParticleManager?.LoadPrefabsFromFile(file); - break; - case ContentType.Decals: - GameMain.DecalManager?.LoadFromFile(file); - break; -#endif - } - - UpdateContentPackageDirtyFlag(file); - } - } - - public void DisableContentPackageItems(IEnumerable unorderedFiles) - { - if (WaitingForAutoUpdate) { return; } - IOrderedEnumerable files = unorderedFiles.OrderBy(ContentFileLoadOrder); - foreach (ContentFile file in files) - { - switch (file.Type) - { - case ContentType.Character: - CharacterPrefab.RemoveByFile(file.Path); - break; - case ContentType.Corpses: - CorpsePrefab.RemoveByFile(file.Path); - break; - case ContentType.NPCConversations: - NPCConversation.RemoveByFile(file.Path); - break; - case ContentType.Jobs: - JobPrefab.RemoveByFile(file.Path); - break; - case ContentType.Item: - ItemPrefab.RemoveByFile(file.Path); - break; - case ContentType.ItemAssembly: - ItemAssemblyPrefab.Remove(file.Path); - break; - case ContentType.Structure: - StructurePrefab.RemoveByFile(file.Path); - break; - case ContentType.Text: - TextManager.RemoveTextPack(file.Path); - break; - case ContentType.Talents: - TalentPrefab.LoadFromFile(file); - break; - case ContentType.TalentTrees: - TalentTree.LoadFromFile(file); - break; -#if CLIENT - case ContentType.Particles: - GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); - break; - case ContentType.Decals: - GameMain.DecalManager?.RemoveByFile(file.Path); - break; -#endif - } - - UpdateContentPackageDirtyFlag(file); - } - } - - public void RefreshContentPackageItems(IEnumerable files) - { - if (WaitingForAutoUpdate) { return; } - if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (files.Any(f => f.Type == ContentType.Submarine || - f.Type == ContentType.Outpost || - f.Type == ContentType.OutpostModule || - f.Type == ContentType.Wreck || - f.Type == ContentType.BeaconStation || - f.Type == ContentType.EnemySubmarine)) { SubmarineInfo.RefreshSavedSubs(); } - if (files.Any(f => f.Type == ContentType.NPCSets)) { NPCSet.LoadSets(); } - if (files.Any(f => f.Type == ContentType.OutpostConfig)) { OutpostGenerationParams.LoadPresets(); } - if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); } - if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); } - if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (files.Any(f => f.Type == ContentType.RandomEvents || - f.Type == ContentType.LocationTypes)) - { - LocationType.List.Clear(); - EventSet.LoadPrefabs(); - LocationType.Init(); - } - if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); } - if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); } - if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); } - if (files.Any(f => f.Type == ContentType.LevelGenerationParameters)) { LevelGenerationParams.LoadPresets(); } - if (files.Any(f => f.Type == ContentType.CaveGenerationParameters)) { CaveGenerationParams.LoadPresets(); } - if (files.Any(f => f.Type == ContentType.TraitorMissions)) { TraitorMissionPrefab.Init(); } - if (files.Any(f => f.Type == ContentType.Orders)) { Order.Init(); } - if (files.Any(f => f.Type == ContentType.EventManagerSettings)) { EventManagerSettings.Init(); } - if (files.Any(f => f.Type == ContentType.WreckAIConfig)) { WreckAIConfig.LoadAll(); } - if (files.Any(f => f.Type == ContentType.SkillSettings)) { SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); } - -#if CLIENT - if (files.Any(f => f.Type == ContentType.Tutorials)) { Tutorial.Init(); } - if (files.Any(f => f.Type == ContentType.Sounds)) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif - } - - private readonly static ContentType[] hotswappableContentTypes = new ContentType[] - { - ContentType.Character, - ContentType.Corpses, - ContentType.NPCConversations, - ContentType.Jobs, - ContentType.Orders, - ContentType.EventManagerSettings, - ContentType.Item, - ContentType.ItemAssembly, - ContentType.Structure, - ContentType.Submarine, - ContentType.Text, - ContentType.Afflictions, - ContentType.RuinConfig, - ContentType.RandomEvents, - ContentType.Missions, - ContentType.LevelObjectPrefabs, - ContentType.LocationTypes, - ContentType.MapGenerationParameters, - ContentType.LevelGenerationParameters, - ContentType.CaveGenerationParameters, - ContentType.Sounds, - ContentType.Particles, - ContentType.Decals, - ContentType.Outpost, - ContentType.OutpostModule, - ContentType.OutpostConfig, - ContentType.NPCSets, - ContentType.Factions, - ContentType.Wreck, - ContentType.WreckAIConfig, - ContentType.BeaconStation, - ContentType.BackgroundCreaturePrefabs, - ContentType.ServerExecutable, - ContentType.TraitorMissions, - ContentType.Tutorials, - ContentType.SkillSettings, - ContentType.None - }; - - private void UpdateContentPackageDirtyFlag(ContentFile file) - { - if (!hotswappableContentTypes.Contains(file.Type)) - { - if (ContentPackage.MultiplayerIncompatibleContent.Contains(file.Type)) - { - ContentPackageSelectionDirty = true; - } - ContentPackageSelectionDirtyNotification = true; - } - } - - public string MasterServerUrl { get; set; } - public string RemoteContentUrl { get; set; } - public bool AutoCheckUpdates { get; set; } - - private string playerName; - public string PlayerName - { - get - { - return string.IsNullOrWhiteSpace(playerName) ? Steam.SteamManager.GetUsername() : playerName; - } - set - { - if (playerName != value) - { - playerName = value; - } - } - } - - public LosMode LosMode - { - get { return losMode; } - set { losMode = value; } - } - - private const float MinHUDScale = 0.75f, MaxHUDScale = 1.25f; - public static float HUDScale { get; set; } - - private const float MinInventoryScale = 0.75f, MaxInventoryScale = 1.25f; - public static float InventoryScale { get; set; } - - private const float MinTextScale = 0.5f, MaxTextScale = 1.5f; - public static float TextScale { get; set; } - private bool textScaleDirty; - - public List CompletedTutorialNames { get; private set; } - /// - /// Identifiers of hints the player has chosen not to see again - /// - public HashSet IgnoredHints { get; private set; } = new HashSet(); - public HashSet EncounteredCreatures { get; private set; } = new HashSet(); - public HashSet KilledCreatures { get; private set; } = new HashSet(); - - public readonly HashSet RecentlyEncounteredCreatures = new HashSet(); - - public static bool VerboseLogging { get; set; } - public static bool SaveDebugConsoleLogs { get; set; } - - public bool CampaignDisclaimerShown, EditorDisclaimerShown; - - public bool ShowLanguageSelectionPrompt { get; set; } - - public static bool ShowOffensiveServerPrompt { get; set; } - - private bool showTutorialSkipWarning = true; - - public static bool EnableSubmarineAutoSave { get; set; } - public static int MaximumAutoSaves { get; set; } - public static int AutoSaveIntervalSeconds { get; set; } - public static Color SubEditorBackgroundColor { get; set; } - public static int SubEditorMaxUndoBuffer { get; set; } - - public bool ShowTutorialSkipWarning - { - get { return showTutorialSkipWarning && CompletedTutorialNames.Count == 0; } - set { showTutorialSkipWarning = value; } - } - - public GameSettings() - { - ContentPackage.LoadAll(); - CompletedTutorialNames = new List(); - - LoadDefaultConfig(); - - LoadPlayerConfig(); - } - - private void LoadDefaultConfig(bool setLanguage = true, bool loadContentPackages = true) - { - XDocument doc = XMLExtensions.TryLoadXml(SavePath); - if (doc == null) - { - GraphicsWidth = 1024; - GraphicsHeight = 768; - MasterServerUrl = ""; - SelectCorePackage(ContentPackage.CorePackages.FirstOrDefault()); - jobPreferences = new List>(); - return; - } - - bool resetLanguage = setLanguage || string.IsNullOrEmpty(Language); - SetDefaultValues(resetLanguage); -#if CLIENT - SetDefaultBindings(doc, legacy: false); -#endif - - MasterServerUrl = doc.Root.GetAttributeString("masterserverurl", MasterServerUrl); - RemoteContentUrl = doc.Root.GetAttributeString("remotecontenturl", RemoteContentUrl); - VerboseLogging = doc.Root.GetAttributeBool("verboselogging", VerboseLogging); - SaveDebugConsoleLogs = doc.Root.GetAttributeBool("savedebugconsolelogs", SaveDebugConsoleLogs); - AutoUpdateWorkshopItems = doc.Root.GetAttributeBool("autoupdateworkshopitems", AutoUpdateWorkshopItems); - - LoadGeneralSettings(doc, resetLanguage); - LoadGraphicSettings(doc); - LoadAudioSettings(doc); -#if CLIENT - LoadControls(doc); - LoadSubEditorImages(doc); -#endif - if (loadContentPackages) - { - LoadContentPackages(doc); - } - -#if DEBUG - WindowMode = WindowMode.Windowed; -#endif - - UnsavedSettings = false; - } - -#region Load PlayerConfig - public void LoadPlayerConfig() - { - bool fileFound = LoadPlayerConfigInternal(); -#if CLIENT - CheckBindings(!fileFound); -#endif - if (!fileFound) - { - ShowLanguageSelectionPrompt = true; - SaveNewPlayerConfig(); - } - } - - // TODO: DRY - /// - /// Returns false if no player config file was found, in which case a new file is created. - /// - private bool LoadPlayerConfigInternal() - { - XDocument doc = XMLExtensions.LoadXml(PlayerSavePath); - if (doc?.Root == null) - { - ShowTutorialSkipWarning = true; - return false; - } - LoadGeneralSettings(doc); - LoadGraphicSettings(doc); - LoadAudioSettings(doc); -#if CLIENT - LoadControls(doc); - LoadSubEditorImages(doc); -#endif - LoadContentPackages(doc); - - //allow overriding the save paths in the config file - if (doc.Root.Attribute("overridesavefolder") != null) - { - overrideSaveFolder = SaveUtil.SaveFolder = doc.Root.GetAttributeString("overridesavefolder", ""); - overrideMultiplayerSaveFolder = SaveUtil.MultiplayerSaveFolder = Path.Combine(overrideSaveFolder, "Multiplayer"); - } - if (doc.Root.Attribute("overridemultiplayersavefolder") != null) - { - overrideMultiplayerSaveFolder = SaveUtil.MultiplayerSaveFolder = doc.Root.GetAttributeString("overridemultiplayersavefolder", ""); - } - - XElement tutorialsElement = doc.Root.Element("tutorials"); - if (tutorialsElement != null) - { - foreach (XElement element in tutorialsElement.Elements()) - { - CompletedTutorialNames.Add(element.GetAttributeString("name", "")); - } - } - - if (doc.Root.Element("ignoredhints") is XElement ignoredHintsElement) - { - IgnoredHints = new HashSet(ignoredHintsElement.GetAttributeStringArray("identifiers", new string[0], convertToLowerInvariant: true)); - } - - XElement encounters = doc.Root.Element("encountered"); - if (encounters != null) - { - EncounteredCreatures = new HashSet(encounters.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true)); - } - XElement kills = doc.Root.Element("killed"); - if (kills != null) - { - KilledCreatures = new HashSet(kills.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true)); - } - - ServerFilterElement = doc.Root.Element("serverfilters"); - - UnsavedSettings = false; - textScaleDirty = false; - return true; - } - -#endregion - -#region Save PlayerConfig - public bool SaveNewPlayerConfig() - { - XDocument doc = new XDocument(); - UnsavedSettings = false; - - if (doc.Root == null) - { - doc.Add(new XElement("config")); - } - - doc.Root.Add( - new XAttribute("gameversion", GameMain.Version.ToString()), - new XAttribute("language", TextManager.Language), - new XAttribute("masterserverurl", MasterServerUrl), - new XAttribute("autocheckupdates", AutoCheckUpdates), - new XAttribute("musicvolume", musicVolume), - new XAttribute("soundvolume", soundVolume), - new XAttribute("verboselogging", VerboseLogging), - new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), - new XAttribute("submarineautosave", EnableSubmarineAutoSave), - new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), - new XAttribute("maxautosaves", MaximumAutoSaves), - new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds), - new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), - new XAttribute("enablesplashscreen", EnableSplashScreen), - new XAttribute("usesteammatchmaking", UseSteamMatchmaking), - new XAttribute("quickstartsub", QuickStartSubmarineName), - new XAttribute("requiresteamauthentication", RequireSteamAuthentication), - new XAttribute("autoupdateworkshopitems", AutoUpdateWorkshopItems), - new XAttribute("pauseonfocuslost", PauseOnFocusLost), - new XAttribute("aimassistamount", aimAssistAmount), - new XAttribute("enablemouselook", EnableMouseLook), - new XAttribute("radialdistortion", EnableRadialDistortion), - new XAttribute("chatopen", ChatOpen), - new XAttribute("crewmenuopen", CrewMenuOpen), - new XAttribute("campaigndisclaimershown", CampaignDisclaimerShown), - new XAttribute("editordisclaimershown", EditorDisclaimerShown), - new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning), - new XAttribute("corpsedespawndelay", CorpseDespawnDelay), - new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold), - new XAttribute("usedualmodesockets", UseDualModeSockets), - new XAttribute("disableingamehints", DisableInGameHints) -#if DEBUG - , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) - , new XAttribute(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled) - , new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled) - , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) - , new XAttribute("modbreakermode", ModBreakerMode) -#endif - ); - - if (!string.IsNullOrEmpty(overrideSaveFolder)) - { - doc.Root.Add(new XAttribute("overridesavefolder", overrideSaveFolder)); - } - if (!string.IsNullOrEmpty(overrideMultiplayerSaveFolder)) - { - doc.Root.Add(new XAttribute("overridemultiplayersavefolder", overrideMultiplayerSaveFolder)); - } - - XElement gMode = doc.Root.Element("graphicsmode"); - if (gMode == null) - { - gMode = new XElement("graphicsmode"); - doc.Root.Add(gMode); - } - - if (GraphicsWidth == 0 || GraphicsHeight == 0) - { - gMode.ReplaceAttributes(new XAttribute("displaymode", windowMode)); - } - else - { - gMode.ReplaceAttributes( - new XAttribute("width", GraphicsWidth), - new XAttribute("height", GraphicsHeight), - new XAttribute("vsync", VSyncEnabled), - new XAttribute("compresstextures", TextureCompressionEnabled), - new XAttribute("framelimit", Timing.FrameLimit), - new XAttribute("displaymode", windowMode)); - } - - XElement audio = doc.Root.Element("audio"); - if (audio == null) - { - audio = new XElement("audio"); - doc.Root.Add(audio); - } - audio.ReplaceAttributes( - new XAttribute("musicvolume", musicVolume), - new XAttribute("soundvolume", soundVolume), - new XAttribute("voicechatvolume", voiceChatVolume), - new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), - new XAttribute("microphonevolume", microphoneVolume), - new XAttribute("muteonfocuslost", MuteOnFocusLost), - new XAttribute("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled), - new XAttribute("voipattenuationenabled", VoipAttenuationEnabled), - new XAttribute("usedirectionalvoicechat", UseDirectionalVoiceChat), - new XAttribute("voicesetting", VoiceSetting), - new XAttribute("audiooutputdevice", System.Xml.XmlConvert.EncodeName(AudioOutputDevice ?? "")), - new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")), - new XAttribute("noisegatethreshold", NoiseGateThreshold), - new XAttribute("uselocalvoicebydefault", UseLocalVoiceByDefault)); - - XElement gSettings = doc.Root.Element("graphicssettings"); - if (gSettings == null) - { - gSettings = new XElement("graphicssettings"); - doc.Root.Add(gSettings); - } - - gSettings.ReplaceAttributes( - new XAttribute("particlelimit", ParticleLimit), - new XAttribute("lightmapscale", LightMapScale), - new XAttribute("chromaticaberration", ChromaticAberrationEnabled), - new XAttribute("losmode", LosMode), - new XAttribute("hudscale", HUDScale), - new XAttribute("inventoryscale", InventoryScale), - new XAttribute("textscale", TextScale)); - - XElement contentPackagesElement = new XElement("contentpackages"); - - string corePackageName = (CurrentCorePackage ?? ContentPackage.CorePackages.FirstOrDefault()).Name; - contentPackagesElement.Add(new XElement("core", new XAttribute("name", corePackageName))); - - XElement regularPackagesElement = new XElement("regular"); - foreach (ContentPackage package in ContentPackage.RegularPackages) - { - XElement packageElement = new XElement("package", new XAttribute("name", package.Name)); - if (EnabledRegularPackages.Contains(package)) { packageElement.Add(new XAttribute("enabled", "true")); } - regularPackagesElement.Add(packageElement); - } - contentPackagesElement.Add(regularPackagesElement); - - doc.Root.Add(contentPackagesElement); - -#if CLIENT - var keyMappingElement = new XElement("keymapping"); - doc.Root.Add(keyMappingElement); - for (int i = 0; i < keyMapping.Length; i++) - { - var key = keyMapping[i]; - if (key == null) { continue; } - if (key.MouseButton == MouseButton.None) - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key)); - } - else - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton)); - } - } - - var inventoryKeyMappingElement = new XElement("inventorykeymapping"); - doc.Root.Add(inventoryKeyMappingElement); - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - KeyOrMouse bind = inventoryKeyMapping[i]; - if (bind.MouseButton == MouseButton.None) - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); - } - else - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); - } - } - - var debugconsoleKeyMappingElement = new XElement("debugconsolemapping"); - doc.Root.Add(debugconsoleKeyMappingElement); - foreach (var (key, command) in ConsoleKeybinds) - { - debugconsoleKeyMappingElement.Add(new XElement("Keybind", - new XAttribute("key", key.ToString()), - new XAttribute("command", command))); - } - - if (ServerFilterElement == null) - { - ShowOffensiveServerPrompt = true; - ServerFilterElement = new XElement("serverfilters"); - } - GameMain.ServerListScreen?.SaveServerFilters(ServerFilterElement); - doc.Root.Add(ServerFilterElement); - - SubEditorScreen.ImageManager.Save(doc.Root); -#endif - - var gameplay = new XElement("gameplay"); - var jobPreferences = new XElement("jobpreferences"); - foreach (Pair job in JobPreferences) - { - XElement jobElement = new XElement("job"); - jobElement.Add(new XAttribute("identifier", job.First)); - jobElement.Add(new XAttribute("variant", job.Second)); - jobPreferences.Add(jobElement); - } - gameplay.Add(jobPreferences); - doc.Root.Add(gameplay); - - var playerElement = new XElement("player", new XAttribute("name", playerName ?? "")); - if (PlayerCharacterCustomization != null) - { - playerElement.SetAttributeValue("headindex", PlayerCharacterCustomization.HeadSpriteId); - if (PlayerCharacterCustomization.gender != Gender.None) { playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender); } - if (PlayerCharacterCustomization.race != Race.None) { playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race); } - playerElement.SetAttributeValue("hairindex", PlayerCharacterCustomization.HairIndex); - playerElement.SetAttributeValue("beardindex", PlayerCharacterCustomization.BeardIndex); - playerElement.SetAttributeValue("moustacheindex", PlayerCharacterCustomization.MoustacheIndex); - playerElement.SetAttributeValue("faceattachmentindex", PlayerCharacterCustomization.FaceAttachmentIndex); - playerElement.SetAttributeValue("skincolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.SkinColor)); - playerElement.SetAttributeValue("haircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.HairColor)); - playerElement.SetAttributeValue("facialhaircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.FacialHairColor)); - } - doc.Root.Add(playerElement); - -#if CLIENT - if (Tutorial.Tutorials != null) - { - foreach (Tutorial tutorial in Tutorial.Tutorials) - { - if (tutorial.Completed && !CompletedTutorialNames.Contains(tutorial.Identifier)) - { - CompletedTutorialNames.Add(tutorial.Identifier); - } - } - } -#endif - var tutorialElement = new XElement("tutorials"); - foreach (string tutorialName in CompletedTutorialNames) - { - tutorialElement.Add(new XElement("Tutorial", new XAttribute("name", tutorialName))); - } - doc.Root.Add(tutorialElement); - - doc.Root.Add(new XElement("ignoredhints", new XAttribute("identifiers", string.Join(",", IgnoredHints).Trim().ToLowerInvariant()))); - - doc.Root.Add(new XElement("encountered", new XAttribute("creatures", string.Join(",", EncounteredCreatures).Trim().ToLowerInvariant()))); - doc.Root.Add(new XElement("killed", new XAttribute("creatures", string.Join(",", KilledCreatures).Trim().ToLowerInvariant()))); - - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings - { - Indent = true, - OmitXmlDeclaration = true, - NewLineOnAttributes = true - }; - - try - { - using (var writer = XmlWriter.Create(PlayerSavePath, settings)) - { - doc.WriteTo(writer); - writer.Flush(); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving game settings failed.", e); - GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, - "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - return false; - } - - return true; - } -#endregion - -#region Loading Configs - private void LoadGeneralSettings(XDocument doc, bool setLanguage = true) - { - if (setLanguage) - { - Language = doc.Root.GetAttributeString("language", Language); - } - AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates); - QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); - EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true); - MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8); - AutoSaveIntervalSeconds = doc.Root.GetAttributeInt("autosaveintervalseconds", 300); - SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f)); - SubEditorMaxUndoBuffer = doc.Root.GetAttributeInt("subeditorundobuffer", 32); - UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking); - RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication); - EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen); - PauseOnFocusLost = doc.Root.GetAttributeBool("pauseonfocuslost", PauseOnFocusLost); - AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", AimAssistAmount); - EnableMouseLook = doc.Root.GetAttributeBool("enablemouselook", EnableMouseLook); - EnableRadialDistortion = doc.Root.GetAttributeBool("radialdistortion", EnableRadialDistortion); - CrewMenuOpen = doc.Root.GetAttributeBool("crewmenuopen", CrewMenuOpen); - ChatOpen = doc.Root.GetAttributeBool("chatopen", ChatOpen); - CorpseDespawnDelay = doc.Root.GetAttributeInt("corpsedespawndelay", 10 * 60); - CorpsesPerSubDespawnThreshold = doc.Root.GetAttributeInt("corpsespersubdespawnthreshold", 5); - CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown); - EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown); - ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true); - UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true); - DisableInGameHints = doc.Root.GetAttributeBool("disableingamehints", DisableInGameHints); -#if DEBUG - AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); - TestScreenEnabled = doc.Root.GetAttributeBool(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled); - AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled); - TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); - ModBreakerMode = doc.Root.GetAttributeBool("modbreakermode", ModBreakerMode); -#endif - XElement gameplayElement = doc.Root.Element("gameplay"); - jobPreferences = new List>(); - if (gameplayElement != null) - { - var preferencesElement = gameplayElement.Element("jobpreferences"); - if (preferencesElement != null) - { - foreach (XElement ele in preferencesElement.Elements("job")) - { - string jobIdentifier = ele.GetAttributeString("identifier", ""); - int outfitVariant = ele.GetAttributeInt("variant", 1); - if (string.IsNullOrEmpty(jobIdentifier)) continue; - jobPreferences.Add(new Pair(jobIdentifier, outfitVariant)); - } - } - - var teamPreferenceElement = gameplayElement.Element("teampreference"); - if (teamPreferenceElement != null) - { - TeamPreference = (CharacterTeamType)Enum.Parse(typeof(CharacterTeamType), teamPreferenceElement.GetAttributeString("team", CharacterTeamType.None.ToString())); - } - } - - XElement playerElement = doc.Root.Element("player"); - if (playerElement != null) - { - playerName = playerElement.GetAttributeString("name", playerName); - int head = playerElement.GetAttributeInt("headindex", -1); - Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender gender); - Enum.TryParse(playerElement.GetAttributeString("race", "none"), true, out Race race); - int hair = playerElement.GetAttributeInt("hairindex", -1); - int beard = playerElement.GetAttributeInt("beardindex", -1); - int moustache = playerElement.GetAttributeInt("moustacheindex", -1); - int faceAttachment = playerElement.GetAttributeInt("faceattachmentindex", -1); - Color skinColor = playerElement.GetAttributeColor("skincolor", Color.Black); - Color hairColor = playerElement.GetAttributeColor("haircolor", Color.Black); - Color facialHairColor = playerElement.GetAttributeColor("facialhaircolor", Color.Black); - PlayerCharacterCustomization = new CharacterInfo.HeadInfo(head, gender, race, hair, beard, moustache, faceAttachment) - { - SkinColor = skinColor, - HairColor = hairColor, - FacialHairColor = facialHairColor - }; - } - } - - private void LoadGraphicSettings(XDocument doc) - { - XElement graphicsMode = doc.Root.Element("graphicsmode"); - GraphicsWidth = graphicsMode.GetAttributeInt("width", GraphicsWidth); - GraphicsHeight = graphicsMode.GetAttributeInt("height", GraphicsHeight); - VSyncEnabled = graphicsMode.GetAttributeBool("vsync", VSyncEnabled); - TextureCompressionEnabled = graphicsMode.GetAttributeBool("compresstextures", TextureCompressionEnabled); - Timing.FrameLimit = graphicsMode.GetAttributeInt("framelimit", 200); - - XElement graphicsSettings = doc.Root.Element("graphicssettings"); - ParticleLimit = graphicsSettings.GetAttributeInt("particlelimit", ParticleLimit); - LightMapScale = MathHelper.Clamp(graphicsSettings.GetAttributeFloat("lightmapscale", LightMapScale), 0.1f, 1.0f); - ChromaticAberrationEnabled = graphicsSettings.GetAttributeBool("chromaticaberration", ChromaticAberrationEnabled); - HUDScale = graphicsSettings.GetAttributeFloat("hudscale", HUDScale); - InventoryScale = graphicsSettings.GetAttributeFloat("inventoryscale", InventoryScale); - TextScale = graphicsSettings.GetAttributeFloat("textscale", TextScale); - var losModeStr = graphicsSettings.GetAttributeString("losmode", "Transparent"); - if (!Enum.TryParse(losModeStr, out losMode)) - { - losMode = LosMode.Transparent; - } -#if CLIENT - if (GraphicsWidth == 0 || GraphicsHeight == 0) - { - GraphicsWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width; - GraphicsHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height; - } -#endif - var windowModeStr = graphicsMode.GetAttributeString("displaymode", "Fullscreen"); - if (!Enum.TryParse(windowModeStr, out windowMode)) - { - windowMode = WindowMode.Fullscreen; - } - } - - private void LoadAudioSettings(XDocument doc) - { - XElement audioSettings = doc.Root.Element("audio"); - if (audioSettings != null) - { - SoundVolume = audioSettings.GetAttributeFloat("soundvolume", SoundVolume); - MusicVolume = audioSettings.GetAttributeFloat("musicvolume", MusicVolume); - DynamicRangeCompressionEnabled = audioSettings.GetAttributeBool("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled); - VoipAttenuationEnabled = audioSettings.GetAttributeBool("voipattenuationenabled", VoipAttenuationEnabled); - VoiceChatVolume = audioSettings.GetAttributeFloat("voicechatvolume", VoiceChatVolume); - VoiceChatCutoffPrevention = audioSettings.GetAttributeInt("voicechatcutoffprevention", VoiceChatCutoffPrevention); - MuteOnFocusLost = audioSettings.GetAttributeBool("muteonfocuslost", MuteOnFocusLost); - - UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat); - VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice)); - AudioOutputDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("audiooutputdevice", AudioOutputDevice)); - NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold); - UseLocalVoiceByDefault = audioSettings.GetAttributeBool("uselocalvoicebydefault", UseLocalVoiceByDefault); - MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume); - string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", ""); - if (Enum.TryParse(voiceSettingStr, out VoiceMode voiceSetting)) - { - VoiceSetting = voiceSetting; - } - } - } - - private void LoadContentPackages(XDocument doc) - { - CurrentCorePackage = null; - enabledRegularPackages.Clear(); - -#if DEBUG && CLIENT - if (ModBreakerMode) - { - CurrentCorePackage = ContentPackage.CorePackages.GetRandom(); - foreach (var regularPackage in ContentPackage.RegularPackages) - { - if (Rand.Range(0.0, 1.0) <= 0.5) - { - enabledRegularPackages.Add(regularPackage); - } - } - ContentPackage.SortContentPackages(p => - { - return Rand.Int(int.MaxValue); - }, config: this); - - if (CurrentCorePackage == null) - { - CurrentCorePackage = ContentPackage.CorePackages.First(); - } - - TextManager.LoadTextPacks(AllEnabledPackages); - return; - } -#endif - - var contentPackagesElement = doc.Root.Element("contentpackages"); - if (contentPackagesElement != null) - { - string coreName = contentPackagesElement.Element("core")?.GetAttributeString("name", ""); - ContentPackage corePackage = ContentPackage.CorePackages.Find(p => p.Name.Equals(coreName, StringComparison.OrdinalIgnoreCase)); - if (corePackage != null) - { - CurrentCorePackage = corePackage; - } - - XElement regularElement = contentPackagesElement.Element("regular"); - - List subElements = regularElement?.Elements()?.ToList(); - if (subElements != null) - { - foreach (var subElement in subElements) - { - if (!bool.TryParse(subElement.GetAttributeString("enabled", "false"), out bool enabled) || !enabled) { continue; } - - string name = subElement.GetAttributeString("name", null); - if (string.IsNullOrEmpty(name)) { continue; } - - var package = ContentPackage.RegularPackages.Find(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - if (package == null) { continue; } - enabledRegularPackages.Add(package); - } - - ContentPackage.SortContentPackages(p => - { - int index = subElements.FindIndex(e => - { - string name = e.GetAttributeString("name", null); - return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase); - }); - return index; - }, config: this); - } - } - else - { - var enabledContentPackagePaths = new List(); - foreach (XElement subElement in doc.Root.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "contentpackage": - string path = subElement.GetAttributeString("path", ""); - enabledContentPackagePaths.Add(path.CleanUpPath().ToLowerInvariant()); - break; - } - } - - foreach (string path in enabledContentPackagePaths) - { - ContentPackage package = ContentPackage.AllPackages - .FirstOrDefault(p => p.Path.CleanUpPath().Equals(path, StringComparison.OrdinalIgnoreCase)); - if (package == null) { continue; } - if (package.IsCorePackage) { CurrentCorePackage = package; } - else { enabledRegularPackages.Add(package); } - } - - ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant()), config: this); - } - - if (CurrentCorePackage == null) - { - CurrentCorePackage = ContentPackage.CorePackages.First(); - } - - TextManager.LoadTextPacks(AllEnabledPackages); - } -#endregion - - public void ResetToDefault() - { - LoadDefaultConfig(); -#if CLIENT - CheckBindings(true); -#endif - SaveNewPlayerConfig(); - } - - private void SetDefaultValues(bool resetLanguage = true) - { - GraphicsWidth = 0; - GraphicsHeight = 0; - VSyncEnabled = true; - TextureCompressionEnabled = true; - Timing.FrameLimit = 200; -#if DEBUG - EnableSplashScreen = false; -#else - EnableSplashScreen = true; -#endif - ParticleLimit = 1500; - LightMapScale = 0.5f; - ChromaticAberrationEnabled = true; - PauseOnFocusLost = true; - MuteOnFocusLost = false; - UseDirectionalVoiceChat = true; - VoiceSetting = VoiceMode.Disabled; - VoiceCaptureDevice = null; - NoiseGateThreshold = -45; - UseLocalVoiceByDefault = false; - windowMode = WindowMode.BorderlessWindowed; - losMode = LosMode.Transparent; - UseSteamMatchmaking = true; - RequireSteamAuthentication = true; - QuickStartSubmarineName = string.Empty; - PlayerCharacterCustomization = null; - aimAssistAmount = 0.5f; - EnableMouseLook = true; - EnableRadialDistortion = true; - CrewMenuOpen = true; - ChatOpen = true; - soundVolume = 0.5f; - musicVolume = 0.3f; - DynamicRangeCompressionEnabled = true; - VoipAttenuationEnabled = true; - voiceChatVolume = 0.5f; - microphoneVolume = 5.0f; - AutoCheckUpdates = true; - playerName = string.Empty; - HUDScale = 1; - InventoryScale = 1; - AutoUpdateWorkshopItems = true; - CampaignDisclaimerShown = false; - CorpseDespawnDelay = 10 * 60; - CorpsesPerSubDespawnThreshold = 5; - if (resetLanguage) - { - Language = "English"; - } - MasterServerUrl = "http://www.undertowgames.com/baromaster"; - VerboseLogging = false; - SaveDebugConsoleLogs = false; - AutoUpdateWorkshopItems = true; - TextScale = 1; - textScaleDirty = false; - DisableInGameHints = false; - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index bdabc58e7..f4818dc48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -27,16 +27,28 @@ namespace Barotrauma protected bool[] IsEquipped; + /// + /// Can the inventory be accessed when the character is still alive + /// public bool AccessibleWhenAlive { get; private set; } + /// + /// Can the inventory be accessed by the character itself when the character is still alive (only has an effect if AccessibleWhenAlive false) + /// + public bool AccessibleByOwner + { + get; + private set; + } + private static string[] ParseSlotTypes(XElement element) { string slotString = element.GetAttributeString("slots", null); - return slotString == null ? new string[0] : slotString.Split(','); + return slotString == null ? Array.Empty() : slotString.Split(','); } public CharacterInventory(XElement element, Character character) @@ -47,6 +59,7 @@ namespace Barotrauma SlotTypes = new InvSlotType[capacity]; AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true); + AccessibleByOwner = element.GetAttributeBool("accessiblebyowner", AccessibleWhenAlive); string[] slotTypeNames = ParseSlotTypes(element); System.Diagnostics.Debug.Assert(slotTypeNames.Length == capacity); @@ -76,7 +89,7 @@ namespace Barotrauma if (GameMain.Client != null) { return; } #endif - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -89,7 +102,15 @@ namespace Barotrauma string slotString = subElement.GetAttributeString("slot", "None"); InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; - Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot, onSpawned: (Item item) => + { + if (item != null && item.ParentInventory != this) + { + string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + }); } } @@ -191,7 +212,7 @@ namespace Barotrauma if (TryPutItem(itemInSameSlot, limbSlot, allowSwapping: false, allowCombine: false, character)) { #if CLIENT - visualSlots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.412f); + visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.412f); #endif } break; @@ -348,7 +369,7 @@ namespace Barotrauma #if CLIENT for (int j = 0; j < capacity; j++) { - if (visualSlots != null && slots[j] == slots[i]) { visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } + if (visualSlots != null && slots[j] == slots[i]) { visualSlots[j].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 9ec10aff0..e34ef8f1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -50,39 +50,39 @@ namespace Barotrauma.Items.Components public int DockingDir { get; set; } - [Serialize("32.0,32.0", false, description: "How close the docking port has to be to another port to dock.")] + [Serialize("32.0,32.0", IsPropertySaveable.No, description: "How close the docking port has to be to another port to dock.")] public Vector2 DistanceTolerance { get; set; } - [Serialize(32.0f, false, description: "How close together the docking ports are forced when docked.")] + [Serialize(32.0f, IsPropertySaveable.No, description: "How close together the docking ports are forced when docked.")] public float DockedDistance { get; set; } - [Serialize(true, false, description: "Is the port horizontal.")] + [Serialize(true, IsPropertySaveable.No, description: "Is the port horizontal.")] public bool IsHorizontal { get; set; } - [Editable, Serialize(false, true, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] public bool MainDockingPort { get; set; } - [Serialize(true, false, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + [Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] public bool ApplyEffectsOnDocking { get; set; } - [Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ - "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] + [Editable, Serialize(DirectionType.None, IsPropertySaveable.No, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ + "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] public DirectionType ForceDockingDirection { get; set; } public DockingPort DockingTarget { get; private set; } @@ -126,11 +126,11 @@ namespace Barotrauma.Items.Components /// public event Action OnUnDocked; - public DockingPort(Item item, XElement element) + public DockingPort(Item item, ContentXElement element) : base(item, element) { // isOpen = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string texturePath = subElement.GetAttributeString("texture", ""); switch (subElement.Name.ToString().ToLowerInvariant()) @@ -338,33 +338,32 @@ namespace Barotrauma.Items.Components Vector2.UnitX * DockingDir : Vector2.UnitY * DockingDir; offset *= DockedDistance * 0.5f * item.Scale; - - Vector2 pos1 = item.WorldPosition + offset; + Vector2 pos1 = item.WorldPosition + offset; Vector2 pos2 = DockingTarget.item.WorldPosition - offset; if (useWeldJoint) { joint = JointFactory.CreateWeldJoint(GameMain.World, item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody, - ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true); + ConvertUnits.ToSimUnits(pos1), ConvertUnits.ToSimUnits(pos2), true); ((WeldJoint)joint).FrequencyHz = 1.0f; + joint.CollideConnected = false; } else { var distanceJoint = JointFactory.CreateDistanceJoint(GameMain.World, item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody, - ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true); + ConvertUnits.ToSimUnits(pos1), ConvertUnits.ToSimUnits(pos2), true); distanceJoint.Length = 0.01f; distanceJoint.Frequency = 1.0f; distanceJoint.DampingRatio = 0.8f; joint = distanceJoint; + joint.CollideConnected = true; } - - joint.CollideConnected = true; } public int GetDir(DockingPort dockingTarget = null) @@ -476,6 +475,10 @@ namespace Barotrauma.Items.Components wire.Connect(powerConnection, false, false); recipient.TryAddLink(wire); wire.Connect(recipient, false, false); + + //Flag connections to be updated + Powered.ChangedConnections.Add(powerConnection); + Powered.ChangedConnections.Add(recipient); } private void CreateDoorBody() @@ -545,7 +548,7 @@ namespace Barotrauma.Items.Components //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = 0; i < 2; i++) { @@ -649,7 +652,7 @@ namespace Barotrauma.Items.Components //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls int upperSubBottom = int.MaxValue, lowerSubTop = int.MinValue; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = 0; i < 2; i++) { @@ -757,7 +760,9 @@ namespace Barotrauma.Items.Components } LinkHullsToGaps(); - + + Item.UpdateHulls(); + hulls[0].ShouldBeSaved = false; hulls[1].ShouldBeSaved = false; item.linkedTo.Add(hulls[0]); @@ -907,6 +912,13 @@ namespace Barotrauma.Items.Components DockingTarget.Undock(); DockingTarget = null; + //Flag power connection + Connection powerConnection = Item.Connections.Find(c => c.IsPower); + if (powerConnection != null) + { + Powered.ChangedConnections.Add(powerConnection); + } + if (doorBody != null) { GameMain.World.Remove(doorBody); @@ -1048,8 +1060,8 @@ namespace Barotrauma.Items.Components if (initialized) { return; } initialized = true; - float maxXDist = (item.Prefab.sprite.size.X * item.Prefab.Scale) / 2; - float closestYDist = (item.Prefab.sprite.size.Y * item.Prefab.Scale) / 2; + float maxXDist = (item.Prefab.Sprite.size.X * item.Prefab.Scale) / 2; + float closestYDist = (item.Prefab.Sprite.size.Y * item.Prefab.Scale) / 2; foreach (Item it in Item.ItemList) { if (it.Submarine != item.Submarine) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 1689b9909..98dc5114d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Items.Components public bool CanBeWelded = true; private float stuck; - [Serialize(0.0f, false, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] public float Stuck { get { return stuck; } @@ -115,13 +115,13 @@ namespace Barotrauma.Items.Components } } - [Serialize(3.0f, true, description: "How quickly the door opens."), Editable] + [Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door opens."), Editable] public float OpeningSpeed { get; private set; } - [Serialize(3.0f, true, description: "How quickly the door closes."), Editable] + [Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door closes."), Editable] public float ClosingSpeed { get; private set; } - [Serialize(1.0f, true, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable] public float ToggleCoolDown { get; private set; } public bool? PredictedState { get; private set; } @@ -155,10 +155,10 @@ namespace Barotrauma.Items.Components public bool IsHorizontal { get; private set; } - [Serialize("0.0,0.0,0.0,0.0", false, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")] + [Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")] public Rectangle Window { get; set; } - [Editable, Serialize(false, true, description: "Is the door currently open.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Is the door currently open.")] public bool IsOpen { get { return isOpen; } @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] + [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } public float OpenState @@ -187,39 +187,39 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")] + [Serialize(false, IsPropertySaveable.No, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")] public bool Impassable { get; set; } - [Editable, Serialize(true, true, description: "", alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "", alwaysUseInstanceValues: true)] public bool UseBetweenOutpostModules { get; private set; } - [Editable, Serialize(false, false, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)] public bool BotsShouldKeepOpen { get; private set; } - public Door(Item item, XElement element) + public Door(Item item, ContentXElement element) : base(item, element) { IsHorizontal = element.GetAttributeBool("horizontal", false); canBePicked = element.GetAttributeBool("canbepicked", false); autoOrientGap = element.GetAttributeBool("autoorientgap", false); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - string texturePath = subElement.GetAttributeString("texture", ""); + string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - doorSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + doorSprite = new Sprite(subElement, path: textureDir); break; case "weldedsprite": - weldedSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + weldedSprite = new Sprite(subElement, path: textureDir); break; case "brokensprite": - brokenSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + brokenSprite = new Sprite(subElement, path: textureDir); scaleBrokenSprite = subElement.GetAttributeBool("scale", false); fadeBrokenSprite = subElement.GetAttributeBool("fade", false); break; @@ -269,15 +269,15 @@ namespace Barotrauma.Items.Components #endif } - private readonly string accessDeniedTxt = TextManager.Get("AccessDenied"); - private readonly string cannotOpenText = TextManager.Get("DoorMsgCannotOpen"); - public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) + private readonly LocalizedString accessDeniedTxt = TextManager.Get("AccessDenied"); + private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen"); + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar"; ParseMsg(); if (addMessage) { - msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText); + msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value; } return isBroken || base.HasRequiredItems(character, addMessage, msg); } @@ -337,7 +337,7 @@ namespace Barotrauma.Items.Components #if CLIENT else if (hasRequiredItems && character != null && character == Character.Controlled) { - GUI.AddMessage(accessDeniedTxt, GUI.Style.Red); + GUI.AddMessage(accessDeniedTxt, GUIStyle.Red); } #endif return false; @@ -561,7 +561,7 @@ namespace Barotrauma.Items.Components { if (!characterPosErrorShown.Contains(c)) { - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); } GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsManager.ErrorSeverity.Error, "Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + c.SimPosition + ")." + " Removed: " + c.Removed + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index eb7b5381c..f4826ee0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -48,28 +48,28 @@ namespace Barotrauma.Items.Components } } - [Serialize(500.0f, true, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] + [Serialize(500.0f, IsPropertySaveable.Yes, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] public float Range { get; set; } - [Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float RangeMultiplierInWalls { get; set; } - [Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Duration { get; set; } - [Serialize(false, true, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] + [Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] public bool OutdoorsOnly { get; @@ -90,12 +90,12 @@ namespace Barotrauma.Items.Components private readonly Attack attack; - public ElectricalDischarger(Item item, XElement element) : + public ElectricalDischarger(Item item, ContentXElement element) : base(item, element) { list.Add(this); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -119,6 +119,7 @@ namespace Barotrauma.Items.Components CurrPowerConsumption = powerConsumption; Voltage = 0.0f; + charging = true; timer = Duration; IsActive = true; @@ -142,10 +143,10 @@ namespace Barotrauma.Items.Components timer -= deltaTime; if (charging) { - if (GetAvailableInstantaneousBatteryPower() >= powerConsumption) + if (GetAvailableInstantaneousBatteryPower() >= PowerConsumption) { - var batteries = item.GetConnectedComponents(); - float neededPower = powerConsumption; + List batteries = GetConnectedBatteries(); + float neededPower = PowerConsumption; while (neededPower > 0.0001f && batteries.Count > 0) { batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); @@ -170,6 +171,14 @@ namespace Barotrauma.Items.Components } } + /// + /// Discharge coil only draws power when charging + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + return charging && IsActive ? PowerConsumption : 0; + } + public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index 5ab363484..9f135fdb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -16,55 +16,55 @@ namespace Barotrauma.Items.Components Circle } - [Editable, Serialize("", true, "Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")] + [Editable, Serialize("", IsPropertySaveable.Yes, "Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")] public string? ItemIdentifier { get; set; } - [Editable, Serialize("", true, "Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")] + [Editable, Serialize("", IsPropertySaveable.Yes, "Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")] public string? SpeciesName { get; set; } - [Editable, Serialize(true, true, "Only spawn if crew members are within certain area")] + [Editable, Serialize(true, IsPropertySaveable.Yes, "Only spawn if crew members are within certain area")] public bool OnlySpawnWhenCrewInRange { get; set; } - [Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where crew members need to stay")] + [Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where crew members need to stay")] public AreaShape CrewAreaShape { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", IsPropertySaveable.Yes, "Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")] public Vector2 CrewAreaBounds { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, IsPropertySaveable.Yes, "Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")] public float CrewAreaRadius { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the crew area from the center of the item")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", IsPropertySaveable.Yes, "Offset of the crew area from the center of the item")] public Vector2 CrewAreaOffset { get; set; } - [Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where enemies or items are spawned")] + [Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where enemies or items are spawned")] public AreaShape SpawnAreaShape { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", IsPropertySaveable.Yes, "Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")] public Vector2 SpawnAreaBounds { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, IsPropertySaveable.Yes, "Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")] public float SpawnAreaRadius { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the spawn area from the center of the item")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", IsPropertySaveable.Yes, "Offset of the spawn area from the center of the item")] public Vector2 SpawnAreaOffset { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 1f), Serialize("10,40", true, "Time range between spawn attempts in seconds. Set both to a negative value to disable automatic spawning.")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 1f), Serialize("10,40", IsPropertySaveable.Yes, "Time range between spawn attempts in seconds. Set both to a negative value to disable automatic spawning.")] public Vector2 SpawnTimerRange { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", IsPropertySaveable.Yes, "Minumum and maximum amount of items or creatures to spawn in one attempt")] public Vector2 SpawnAmountRange { get; set; } - [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, IsPropertySaveable.Yes, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")] public int MaximumAmount { get; set; } - [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, IsPropertySaveable.Yes, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")] public int MaximumAmountInArea { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, IsPropertySaveable.Yes, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] public float MaximumAmountRangePadding { get; set; } - [Serialize(true, true, "")] + [Serialize(true, IsPropertySaveable.Yes, "")] public bool CanSpawn { get; set; } = true; private float spawnTimer; @@ -72,7 +72,7 @@ namespace Barotrauma.Items.Components private int spawnedAmount = 0; - public EntitySpawnerComponent(Item item, XElement element) : base(item, element) + public EntitySpawnerComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; } @@ -90,7 +90,7 @@ namespace Barotrauma.Items.Components foreach (ItemPrefab prefab in ItemPrefab.Prefabs) { - if (string.Equals(trimmedString, prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + if (trimmedString == prefab.Identifier) { found = true; break; @@ -182,11 +182,11 @@ namespace Barotrauma.Items.Components int amount; if (!string.IsNullOrWhiteSpace(SpeciesName)) { - amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true)); + amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName == SpeciesName && IsInRange(c.WorldPosition, crewArea: false, rangePad: true)); } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { - amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.OrdinalIgnoreCase) && IsInRange(it.WorldPosition, crewArea: false, rangePad: true)); + amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier == ItemIdentifier && IsInRange(it.WorldPosition, crewArea: false, rangePad: true)); } else { @@ -270,15 +270,15 @@ namespace Barotrauma.Items.Components { if (!string.IsNullOrWhiteSpace(SpeciesName)) { - string[] allSpecies = SpeciesName.Split(','); - string species = allSpecies.GetRandom().Trim(); - Entity.Spawner?.AddToSpawnQueue(species, pos); + Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifier species = allSpecies.GetRandomUnsynced(); + Entity.Spawner?.AddCharacterToSpawnQueue(species, pos); spawnedAmount++; } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { - string[] allItems = ItemIdentifier.Split(','); - string itemIdentifier = allItems.GetRandom().Trim(); + Identifier[] allItems = ItemIdentifier.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifier itemIdentifier = allItems.GetRandomUnsynced(); ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier); if (prefab is null) { return; } @@ -287,7 +287,7 @@ namespace Barotrauma.Items.Components pos -= sub.Position; } - Entity.Spawner?.AddToSpawnQueue(prefab, pos, item.Submarine); + Entity.Spawner?.AddItemToSpawnQueue(prefab, pos, item.Submarine); spawnedAmount++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index c8b8782ba..d8543526e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -10,27 +10,27 @@ namespace Barotrauma.Items.Components { partial class GeneticMaterial : ItemComponent, IServerSerializable { - private readonly string materialName; + private readonly LocalizedString materialName; private Character targetCharacter; private AfflictionPrefab selectedEffect, selectedTaintedEffect; - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Effect { get; set; } - [Serialize("geneticmaterialdebuff", true)] - public string TaintedEffect + [Serialize("geneticmaterialdebuff", IsPropertySaveable.Yes)] + public Identifier TaintedEffect { get; set; } private bool tainted; - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool Tainted { get { return tainted; } @@ -39,28 +39,27 @@ namespace Barotrauma.Items.Components if (!value) { return; } tainted = true; item.AllowDeconstruct = false; - if (!string.IsNullOrEmpty(TaintedEffect)) + if (!TaintedEffect.IsEmpty) { selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a => - a.Identifier.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase) || - a.AfflictionType.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + a.Identifier == TaintedEffect || + a.AfflictionType == TaintedEffect).GetRandomUnsynced(); } } } //only for saving the selected tainted effect - [Serialize("", true)] - public string SelectedTaintedEffect + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SelectedTaintedEffect { - get { return selectedTaintedEffect?.Identifier ?? string.Empty; } + get { return selectedTaintedEffect?.Identifier ?? Identifier.Empty; } private set { - if (string.IsNullOrEmpty(value)) { return; } - selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.Identifier == value); + selectedTaintedEffect = !value.IsEmpty ? AfflictionPrefab.Prefabs.Find(a => a.Identifier == value) : null; } } - public GeneticMaterial(Item item, XElement element) + public GeneticMaterial(Item item, ContentXElement element) : base(item, element) { string nameId = element.GetAttributeString("nameidentifier", ""); @@ -71,15 +70,15 @@ namespace Barotrauma.Items.Components if (!string.IsNullOrEmpty(Effect)) { selectedEffect = AfflictionPrefab.Prefabs.Where(a => - a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) || - a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + a.Identifier == Effect || + a.AfflictionType == Effect).GetRandomUnsynced(); } } - [Serialize(3.0f, false)] + [Serialize(3.0f, IsPropertySaveable.No)] public float ConditionIncreaseOnCombineMin { get; set; } - [Serialize(8.0f, false)] + [Serialize(8.0f, IsPropertySaveable.No)] public float ConditionIncreaseOnCombineMax { get; set; } public bool CanBeCombinedWith(GeneticMaterial otherGeneticMaterial) @@ -230,16 +229,16 @@ namespace Barotrauma.Items.Components #endif } - public static string TryCreateName(ItemPrefab prefab, XElement element) + public static LocalizedString TryCreateName(ItemPrefab prefab, XElement element) { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().Equals(nameof(GeneticMaterial), StringComparison.OrdinalIgnoreCase)) + if (subElement.NameAsIdentifier() == nameof(GeneticMaterial)) { - string nameId = subElement.GetAttributeString("nameidentifier", ""); - if (!string.IsNullOrEmpty(nameId)) + Identifier nameId = subElement.GetAttributeIdentifier("nameidentifier", ""); + if (!nameId.IsEmpty) { - return prefab.Name.Replace("[type]", TextManager.Get(nameId, returnNull: true) ?? nameId); + return prefab.Name.Replace("[type]", TextManager.Get(nameId).Fallback(nameId.Value)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index a9bd794cf..5cde984fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -15,25 +15,30 @@ namespace Barotrauma.Items.Components { internal class ProducedItem { - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Probability { get; set; } public readonly List StatusEffects = new List(); + public readonly Item Producer; + public readonly ItemPrefab? Prefab; - public ProducedItem(ItemPrefab prefab, float probability) + public ProducedItem(Item producer, ItemPrefab prefab, float probability) { + Producer = producer; Prefab = prefab; Probability = probability; } - public ProducedItem(XElement element) + public ProducedItem(Item producer, ContentXElement element) { SerializableProperty.DeserializeProperties(this, element); - string itemIdentifier = element.GetAttributeString("identifier", string.Empty); - if (!string.IsNullOrWhiteSpace(itemIdentifier)) + Producer = producer; + + Identifier itemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (!itemIdentifier.IsEmpty) { Prefab = ItemPrefab.Find(null, itemIdentifier); } @@ -41,17 +46,17 @@ namespace Barotrauma.Items.Components LoadSubElements(element); } - private void LoadSubElements(XElement element) + private void LoadSubElements(ContentXElement element) { if (!element.HasElements) { return; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": { - StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name); + StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value); if (effect.type != ActionType.OnProduceSpawned) { DebugConsole.ThrowError("Only OnProduceSpawned type can be used in ."); @@ -325,9 +330,14 @@ namespace Barotrauma.Items.Components } public static TileSide GetOppositeSide(this TileSide side) - { - return (TileSide) (1 << ((int) Math.Log2((int) side) + 2) % 4); - } + => side switch + { + TileSide.Left => TileSide.Right, + TileSide.Right => TileSide.Left, + TileSide.Bottom => TileSide.Top, + TileSide.Top => TileSide.Bottom, + _ => throw new ArgumentException($"Expected Left, Right, Bottom or Top, got {side}") + }; } internal partial class Growable : ItemComponent, IServerSerializable @@ -335,61 +345,61 @@ namespace Barotrauma.Items.Components // used for debugging where a vine failed to grow public readonly HashSet FailedRectangles = new HashSet(); - [Serialize(1f, true, "How fast the plant grows.")] + [Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows.")] public float GrowthSpeed { get; set; } - [Serialize(100f, true, "How long the plant can go without watering.")] + [Serialize(100f, IsPropertySaveable.Yes, "How long the plant can go without watering.")] public float MaxHealth { get; set; } - [Serialize(1f, true, "How much damage the plant takes while in water.")] + [Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while in water.")] public float FloodTolerance { get; set; } - [Serialize(1f, true, "How much damage the plant takes while growing.")] + [Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while growing.")] public float Hardiness { get; set; } - [Serialize(0.01f, true, "How often a seed is produced.")] + [Serialize(0.01f, IsPropertySaveable.Yes, "How often a seed is produced.")] public float SeedRate { get; set; } - [Serialize(0.01f, true, "How often a product item is produced.")] + [Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced.")] public float ProductRate { get; set; } - [Serialize(0.5f, true, "Probability of an attribute being randomly modified in a newly produced seed.")] + [Serialize(0.5f, IsPropertySaveable.Yes, "Probability of an attribute being randomly modified in a newly produced seed.")] public float MutationProbability { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, "Color of the flowers.")] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the flowers.")] public Color FlowerTint { get; set; } - [Serialize(3, true, "Number of flowers drawn when fully grown")] + [Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn when fully grown")] public int FlowerQuantity { get; set; } - [Serialize(0.25f, true, "Size of the flower sprites.")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Size of the flower sprites.")] public float BaseFlowerScale { get; set; } - [Serialize(0.5f, true, "Size of the leaf sprites.")] + [Serialize(0.5f, IsPropertySaveable.Yes, "Size of the leaf sprites.")] public float BaseLeafScale { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, "Color of the leaves.")] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the leaves.")] public Color LeafTint { get; set; } - [Serialize(0.33f, true, "Chance of a leaf appearing behind a branch.")] + [Serialize(0.33f, IsPropertySaveable.Yes, "Chance of a leaf appearing behind a branch.")] public float LeafProbability { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, "Color of the vines.")] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the vines.")] public Color VineTint { get; set; } - [Serialize(32, true, "Maximum number of vine tiles the plant can grow.")] + [Serialize(32, IsPropertySaveable.Yes, "Maximum number of vine tiles the plant can grow.")] public int MaximumVines { get; set; } - [Serialize(0.25f, true, "Size of the vine sprites.")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Size of the vine sprites.")] public float VineScale { get; set; } - [Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")] + [Serialize("0.26,0.27,0.29,1.0", IsPropertySaveable.Yes, "Tint of a dead plant.")] public Color DeadTint { get; set; } - [Serialize("1,1,1,1", true, "Probability for the plant to grow in a direction.")] + [Serialize("1,1,1,1", IsPropertySaveable.Yes, "Probability for the plant to grow in a direction.")] public Vector4 GrowthWeights { get; set; } - [Serialize(0.0f, true, "How much damage is taken from fires.")] + [Serialize(0.0f, IsPropertySaveable.Yes, "How much damage is taken from fires.")] public float FireVulnerability { get; set; } private const float increasedDeathSpeed = 10f; @@ -422,7 +432,7 @@ namespace Barotrauma.Items.Components private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f; private const int VineChunkSize = 32; - public Growable(Item item, XElement element) : base(item, element) + public Growable(Item item, ContentXElement element) : base(item, element) { SerializableProperty.DeserializeProperties(this, element); @@ -430,12 +440,12 @@ namespace Barotrauma.Items.Components if (element.HasElements) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "produceditem": - ProducedItems.Add(new ProducedItem(subElement)); + ProducedItems.Add(new ProducedItem(this.item, subElement)); break; case "vinesprites": LoadVines(subElement); @@ -444,7 +454,7 @@ namespace Barotrauma.Items.Components } } - ProducedSeed = new ProducedItem(this.item.Prefab, 1.0f); + ProducedSeed = new ProducedItem(this.item, this.item.Prefab, 1.0f); flowerTiles = new int[FlowerQuantity]; } @@ -471,7 +481,7 @@ namespace Barotrauma.Items.Components } } - partial void LoadVines(XElement element); + partial void LoadVines(ContentXElement element); public void OnGrowthTick(Planter planter, PlantSlot slot) { @@ -541,7 +551,7 @@ namespace Barotrauma.Items.Components if (spawnProduct || spawnSeed) { - VineTile vine = Vines.GetRandom(); + VineTile vine = Vines.GetRandomUnsynced(); spawnPos = vine.GetWorldPosition(planter, slot.Offset); } else @@ -564,9 +574,9 @@ namespace Barotrauma.Items.Components { if (producedItem.Prefab == null) { return; } - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningProduce:" + thisItem.prefab.Identifier + ":" + producedItem.Prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningProduce:" + thisItem.Prefab.Identifier + ":" + producedItem.Prefab.Identifier); - Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it => + Entity.Spawner?.AddItemToSpawnQueue(producedItem.Prefab, pos, onSpawned: it => { foreach (StatusEffect effect in producedItem.StatusEffects) { @@ -590,7 +600,7 @@ namespace Barotrauma.Items.Components { if (!Decayed) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningDied:" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningDied:" + item.Prefab.Identifier); } Decayed = true; @@ -881,14 +891,14 @@ namespace Barotrauma.Items.Components return element; } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); - flowerTiles = componentElement.GetAttributeIntArray("flowertiles", new int[0]); + flowerTiles = componentElement.GetAttributeIntArray("flowertiles", Array.Empty())!; Decayed = componentElement.GetAttributeBool("decayed", false); Vines.Clear(); - foreach (XElement element in componentElement.Elements()) + foreach (var element in componentElement.Elements()) { if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 7d623bc39..b8caed282 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components private readonly Vector2[] scaledHandlePos; private readonly InputType prevPickKey; - private string prevMsg; + private LocalizedString prevMsg; private Dictionary> prevRequiredItems; //the distance from the holding characters elbow to center of the physics body of the item @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components get; private set; } - [Serialize(true, true, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")] + [Serialize(true, IsPropertySaveable.Yes, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")] public bool CanPush { get; @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components get { return item.body ?? body; } } - [Serialize(false, true, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")] public bool Attached { get { return attached && item.ParentInventory == null; } @@ -65,56 +65,56 @@ namespace Barotrauma.Items.Components } } - [Serialize(true, true, description: "Can the item be pointed to a specific direction or do the characters always hold it in a static pose.")] + [Serialize(true, IsPropertySaveable.Yes, description: "Can the item be pointed to a specific direction or do the characters always hold it in a static pose.")] public bool Aimable { get; set; } - [Serialize(false, false, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")] public bool ControlPose { get; set; } - [Serialize(false, false, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")] + [Serialize(false, IsPropertySaveable.No, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")] public bool UseHandRotationForHoldAngle { get; set; } - [Serialize(false, false, description: "Can the item be attached to walls.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be attached to walls.")] public bool Attachable { get { return attachable; } set { attachable = value; } } - [Serialize(true, false, description: "Can the item be reattached to walls after it has been deattached (only valid if Attachable is set to true).")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item be reattached to walls after it has been deattached (only valid if Attachable is set to true).")] public bool Reattachable { get; set; } - [Serialize(false, false, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")] public bool LimitedAttachable { get; set; } - [Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")] public bool AttachedByDefault { get { return attachedByDefault; } set { attachedByDefault = value; } } - [Editable, Serialize("0.0,0.0", false, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ " For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")] public Vector2 HoldPos { @@ -122,7 +122,7 @@ namespace Barotrauma.Items.Components set { holdPos = ConvertUnits.ToSimUnits(value); } } - [Serialize("0.0,0.0", false, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+ + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+ " Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")] public Vector2 AimPos { @@ -130,7 +130,7 @@ namespace Barotrauma.Items.Components set { aimPos = ConvertUnits.ToSimUnits(value); } } - [Editable, Serialize(0.0f, false, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] public float HoldAngle { get { return MathHelper.ToDegrees(holdAngle); } @@ -138,24 +138,31 @@ namespace Barotrauma.Items.Components } private Vector2 swingAmount; - [Editable, Serialize("0.0,0.0", false, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] public Vector2 SwingAmount { get { return ConvertUnits.ToDisplayUnits(swingAmount); } set { swingAmount = ConvertUnits.ToSimUnits(value); } } - [Editable, Serialize(0.0f, false, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] public float SwingSpeed { get; set; } - [Editable, Serialize(false, false, description: "Should the item swing around when it's being held.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")] public bool SwingWhenHolding { get; set; } - [Editable, Serialize(false, false, description: "Should the item swing around when it's being aimed.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")] public bool SwingWhenAiming { get; set; } - [Editable, Serialize(false, false, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] public bool SwingWhenUsing { get; set; } - - public Holdable(Item item, XElement element) + + [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.85f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is attached to a wall.")] + public float SpriteDepthWhenAttached + { + get; + set; + } + + public Holdable(Item item, ContentXElement element) : base(item, element) { body = item.body; @@ -238,7 +245,7 @@ namespace Barotrauma.Items.Components } private bool loadedFromXml; - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); @@ -523,6 +530,11 @@ namespace Barotrauma.Items.Components { if (!attachable) { return; } + if (body == null) + { + throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier})."); + } + //outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub if (item.CurrentHull == null && item.Submarine == null) { @@ -570,6 +582,9 @@ namespace Barotrauma.Items.Components requiredItems = new Dictionary>(prevRequiredItems); Attached = true; +#if CLIENT + item.DrawDepthOffset = SpriteDepthWhenAttached - item.SpriteDepth; +#endif } public void DeattachFromWall() @@ -578,7 +593,9 @@ namespace Barotrauma.Items.Components Attached = false; attachTargetCell = null; - +#if CLIENT + item.DrawDepthOffset = 0.0f; +#endif //make the item pickable with the default pick key and with no specific tools/items when it's deattached requiredItems.Clear(); DisplayMsg = ""; @@ -615,7 +632,7 @@ namespace Barotrauma.Items.Components int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); int currentlyAttachedCount = Item.ItemList.Count( - i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); + i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); if (maxAttachableCount == 0) { #if CLIENT @@ -623,7 +640,7 @@ namespace Barotrauma.Items.Components #endif return false; } - else if (currentlyAttachedCount >= maxAttachableCount) + else if (currentlyAttachedCount >= maxAttachableCount) { #if CLIENT GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index 8fcf6d265..68683091a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -1,64 +1,113 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma.Items.Components { partial class IdCard : Pickable { - [Serialize(CharacterTeamType.None, true, alwaysUseInstanceValues: true)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public CharacterTeamType TeamID { get; set; } - [Serialize(0, true, alwaysUseInstanceValues: true)] + [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int SubmarineSpecificID { get; set; } + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string OwnerTags + { + get => string.Join(',', OwnerTagSet); + set => OwnerTagSet = value.Split(',').ToIdentifiers().ToImmutableHashSet(); + } + + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string Description + { + get; + set; + } + private JobPrefab cachedJobPrefab; private string cachedName; - public IdCard(Item item, XElement element) : base(item, element) - { + public ImmutableHashSet OwnerTagSet { get; set; } - } + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string OwnerName { get; set; } + + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Identifier OwnerJobId { get; set; } - public void Initialize(CharacterInfo info) + public JobPrefab OwnerJob => JobPrefab.Prefabs.TryGet(OwnerJobId, out var prefab) ? prefab : null; + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerHairIndex { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerBeardIndex { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerMoustacheIndex { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerFaceAttachmentIndex { get; set; } + + [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Color OwnerHairColor { get; set; } + + [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Color OwnerFacialHairColor { get; set; } + + [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Color OwnerSkinColor { get; set; } + + #warning TODO: figure out how to set Vector2.Zero as the default here + [Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Vector2 OwnerSheetIndex { get; set; } + + public IdCard(Item item, ContentXElement element) : base(item, element) { } + + public void Initialize(WayPoint spawnPoint, Character character) { + item.AddTag("name:" + character.Name); + + CharacterInfo info = character.Info; if (info == null) { return; } - if (info.Job?.Prefab != null) + if (spawnPoint != null) { - item.AddTag("jobid:" + info.Job.Prefab.Identifier); + foreach (string s in spawnPoint.IdCardTags) + { + item.AddTag(s); + } + if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) + { + item.Description = Description = spawnPoint.IdCardDesc; + } } TeamID = info.TeamID; var head = info.Head; - if (head == null) { return; } - - if (info.HasGenders) { item.AddTag($"gender:{head.gender.ToString().ToLowerInvariant()}"); } - if (info.HasRaces) { item.AddTag($"race:{head.race}"); } - item.AddTag($"headspriteid:{info.HeadSpriteId}"); - item.AddTag($"hairindex:{head.HairIndex}"); - item.AddTag($"beardindex:{head.BeardIndex}"); - item.AddTag($"moustacheindex:{head.MoustacheIndex}"); - item.AddTag($"faceattachmentindex:{head.FaceAttachmentIndex}"); - item.AddTag($"haircolor:{head.HairColor.ToStringHex()}"); - item.AddTag($"facialhaircolor:{head.FacialHairColor.ToStringHex()}"); - item.AddTag($"skincolor:{head.SkinColor.ToStringHex()}"); - if (head.SheetIndex != null) - { - item.AddTag($"sheetindex:{head.SheetIndex.Value.X};{head.SheetIndex.Value.Y}"); - } + OwnerName = info.Name; + OwnerJobId = info.Job?.Prefab.Identifier ?? Identifier.Empty; + OwnerTagSet = info.Head.Preset.TagSet; + OwnerHairIndex = head.HairIndex; + OwnerBeardIndex = head.BeardIndex; + OwnerMoustacheIndex = head.MoustacheIndex; + OwnerFaceAttachmentIndex = head.FaceAttachmentIndex; + OwnerHairColor = head.HairColor; + OwnerFacialHairColor = head.FacialHairColor; + OwnerSkinColor = head.SkinColor; + OwnerSheetIndex = head.SheetIndex; } public override void Equip(Character character) @@ -72,48 +121,12 @@ namespace Barotrauma.Items.Components base.Unequip(character); character.Info?.CheckDisguiseStatus(true, this); } - - public JobPrefab GetJob() + public override void OnItemLoaded() { - if (cachedJobPrefab != null) + if (!string.IsNullOrEmpty(Description)) { - return cachedJobPrefab; + item.Description = Description; } - - foreach (string tag in item.GetTags()) - { - if (tag.StartsWith("jobid:")) - { - string jobIdentifier = tag.Split(':').Last(); - if (JobPrefab.Get(jobIdentifier) is { } jobPrefab) - { - cachedJobPrefab = jobPrefab; - return jobPrefab; - } - } - } - - return null; - } - - public string GetName() - { - if (cachedName != null) - { - return cachedName; - } - - foreach (string tag in item.GetTags()) - { - if (tag.StartsWith("name:")) - { - string ownerName = tag.Split(':').Last(); - cachedName = ownerName; - return ownerName; - } - } - - return null; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 319c60ce4..0dee7a4a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -15,14 +15,14 @@ namespace Barotrauma.Items.Components private float deattachTimer; - [Serialize(1.0f, false, description: "How long it takes to deattach the item from the level walls (in seconds).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How long it takes to deattach the item from the level walls (in seconds).")] public float DeattachDuration { get; set; } - [Serialize(0.0f, false, description: "How far along the item is to being deattached. When the timer goes above DeattachDuration, the item is deattached.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How far along the item is to being deattached. When the timer goes above DeattachDuration, the item is deattached.")] public float DeattachTimer { get { return deattachTimer; } @@ -53,7 +53,7 @@ namespace Barotrauma.Items.Components { if (holdable.Attached) { - GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.Prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier); holdable.DeattachFromWall(); } trigger.Enabled = false; @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(1.0f, false, description: "How much the position of the item can vary from the wall the item spawns on.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the position of the item can vary from the wall the item spawns on.")] public float RandomOffsetFromWall { get; @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components get { return holdable != null && holdable.Attached; } } - public LevelResource(Item item, XElement element) : base(item, element) + public LevelResource(Item item, ContentXElement element) : base(item, element) { IsActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index ad88b23d0..95152d686 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -29,34 +29,34 @@ namespace Barotrauma.Items.Components public Character User { get; private set; } - [Serialize(0.0f, false, description: "An estimation of how close the item has to be to the target for it to hit. Used by AI characters to determine when they're close enough to hit a target.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "An estimation of how close the item has to be to the target for it to hit. Used by AI characters to determine when they're close enough to hit a target.")] public float Range { get { return ConvertUnits.ToDisplayUnits(range); } set { range = ConvertUnits.ToSimUnits(value); } } - [Serialize(0.5f, false, description: "How long the user has to wait before they can hit with the weapon again (in seconds).")] + [Serialize(0.5f, IsPropertySaveable.No, description: "How long the user has to wait before they can hit with the weapon again (in seconds).")] public float Reload { get { return reload; } set { reload = Math.Max(0.0f, value); } } - [Serialize(false, false, description: "Can the weapon hit multiple targets per swing.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the weapon hit multiple targets per swing.")] public bool AllowHitMultiple { get; set; } - [Editable, Serialize(true, false)] + [Editable, Serialize(true, IsPropertySaveable.No)] public bool Swing { get; set; } - [Editable, Serialize("2.0, 0.0", false)] + [Editable, Serialize("2.0, 0.0", IsPropertySaveable.No)] public Vector2 SwingPos { get; set; } - [Editable, Serialize("3.0, -1.0", false)] + [Editable, Serialize("3.0, -1.0", IsPropertySaveable.No)] public Vector2 SwingForce { get; set; } public bool Hitting { get { return hitting; } } @@ -64,12 +64,12 @@ namespace Barotrauma.Items.Components /// /// Defines items that boost the weapon functionality, like battery cell for stun batons. /// - public readonly string[] PreferredContainedItems; + public readonly Identifier[] PreferredContainedItems; - public MeleeWeapon(Item item, XElement element) + public MeleeWeapon(Item item, ContentXElement element) : base(item, element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item) @@ -79,7 +79,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); - PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true); + PreferredContainedItems = element.GetAttributeIdentifierArray("preferredcontaineditems", Array.Empty()); } public override void Equip(Character character) @@ -461,7 +461,7 @@ namespace Barotrauma.Items.Components if (DeleteOnUse) { - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 93001af68..432ae93bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -36,8 +36,8 @@ namespace Barotrauma.Items.Components return picker; } } - - public Pickable(Item item, XElement element) + + public Pickable(Item item, ContentXElement element) : base(item, element) { allowedSlots = new List(); @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components this, item.WorldPosition, pickTimer / requiredTime, - GUI.Style.Red, GUI.Style.Green, + GUIStyle.Red, GUIStyle.Green, !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 39e61fcb0..971068a95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -17,15 +17,15 @@ namespace Barotrauma.Items.Components private float useState; - [Serialize(UseEnvironment.Both, false, description: "Can the item be used in air, underwater or both.")] + [Serialize(UseEnvironment.Both, IsPropertySaveable.No, description: "Can the item be used in air, underwater or both.")] public UseEnvironment UsableIn { get; set; } - [Serialize(0.0f, false, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; set; } #if CLIENT private string particles; - [Serialize("", false, description: "The name of the particle prefab the item emits when used.")] + [Serialize("", IsPropertySaveable.No, description: "The name of the particle prefab the item emits when used.")] public string Particles { get { return particles; } @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components } #endif - public Propulsion(Item item, XElement element) + public Propulsion(Item item, ContentXElement element) : base(item,element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index e3ff2e8be..339d218aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -18,49 +18,49 @@ namespace Barotrauma.Items.Components private Vector2 barrelPos; - [Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels). Determines where the projectiles spawn.")] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position of the barrel as an offset from the item's center (in pixels). Determines where the projectiles spawn.")] public string BarrelPos { get { return XMLExtensions.Vector2ToString(ConvertUnits.ToDisplayUnits(barrelPos)); } set { barrelPos = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(value)); } } - [Serialize(1.0f, false, description: "How long the user has to wait before they can fire the weapon again (in seconds).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How long the user has to wait before they can fire the weapon again (in seconds).")] public float Reload { get { return reload; } set { reload = Math.Max(value, 0.0f); } } - [Serialize(false, false, description: "Tells the AI to hold the trigger down when it uses this weapon")] + [Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")] public bool HoldTrigger { get; set; } - [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + [Serialize(1, IsPropertySaveable.No, description: "How projectiles the weapon launches when fired once.")] public int ProjectileCount { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")] public float Spread { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with insufficient skills to use the weapon (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles when used by a character with insufficient skills to use the weapon (in degrees).")] public float UnskilledSpread { get; set; } - [Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")] + [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] public float MaxChargeTime { get; @@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components private float currentChargeTime; private bool tryingToCharge; - public RangedWeapon(Item item, XElement element) + public RangedWeapon(Item item, ContentXElement element) : base(item, element) { item.IsShootable = true; @@ -102,7 +102,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Equip(Character character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index e172945b6..9477d5a76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components Air, Water, Both, None }; - private readonly List fixableEntities; + private readonly HashSet fixableEntities; private Vector2 pickedPosition; private float activeTimer; @@ -25,88 +25,88 @@ namespace Barotrauma.Items.Components private readonly List ignoredBodies = new List(); - [Serialize("Both", false, description: "Can the item be used in air, water or both.")] + [Serialize("Both", IsPropertySaveable.No, description: "Can the item be used in air, water or both.")] public UseEnvironment UsableIn { get; set; } - [Serialize(0.0f, false, description: "The distance at which the item can repair targets.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The distance at which the item can repair targets.")] public float Range { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")] public float Spread { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")] public float UnskilledSpread { get; set; } - [Serialize(0.0f, false, description: "How many units of damage the item removes from structures per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from structures per second.")] public float StructureFixAmount { get; set; } - [Serialize(0.0f, false, description: "How much damage is applied to ballast flora.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much damage is applied to ballast flora.")] public float FireDamage { get; set; } - [Serialize(0.0f, false, description: "How many units of damage the item removes from destructible level walls per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from destructible level walls per second.")] public float LevelWallFixAmount { get; set; } - [Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the item decreases the size of fires per second.")] public float ExtinguishAmount { get; set; } - [Serialize(0.0f, false, description: "How much water the item provides to planters per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much water the item provides to planters per second.")] public float WaterAmount { get; set; } - [Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position of the barrel as an offset from the item's center (in pixels).")] public Vector2 BarrelPos { get; set; } - [Serialize(false, false, description: "Can the item repair things through walls.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through walls.")] public bool RepairThroughWalls { get; set; } - [Serialize(false, false, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")] public bool RepairMultiple { get; set; } - [Serialize(false, false, description: "Can the item repair things through holes in walls.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } - [Serialize(100.0f, false, description: "How far two walls need to not be considered overlapping and to stop the ray.")] + [Serialize(100.0f, IsPropertySaveable.No, description: "How far two walls need to not be considered overlapping and to stop the ray.")] public float MaxOverlappingWallDist { get; set; } - [Serialize(true, false, description: "Can the item hit broken doors.")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitItems { get; set; } - [Serialize(false, false, description: "Can the item hit broken doors.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitBrokenDoors { get; set; } - [Serialize(0.0f, false, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } - [Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Force applied to the entity the ray hits.")] public float TargetForce { get; set; } - [Serialize(0.0f, false, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })] + [Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })] public float BarrelRotation { get; set; @@ -124,7 +124,7 @@ namespace Barotrauma.Items.Components } } - public RepairTool(Item item, XElement element) + public RepairTool(Item item, ContentXElement element) : base(item, element) { this.item = item; @@ -134,8 +134,8 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute."); } - fixableEntities = new List(); - foreach (XElement subElement in element.Elements()) + fixableEntities = new HashSet(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -143,11 +143,11 @@ namespace Barotrauma.Items.Components if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities."); - fixableEntities.Add(subElement.Attribute("name").Value); + fixableEntities.Add(subElement.Attribute("name").Value.ToIdentifier()); } else { - fixableEntities.Add(subElement.GetAttributeString("identifier", "")); + fixableEntities.Add(subElement.GetAttributeIdentifier("identifier", "")); } break; } @@ -157,7 +157,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -489,7 +489,7 @@ namespace Barotrauma.Items.Components #if CLIENT float barOffset = 10f * GUI.Scale; Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero; - user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering"); + user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering"); #endif } } @@ -625,7 +625,7 @@ namespace Barotrauma.Items.Components this, targetItem.WorldPosition, levelResource.DeattachTimer / levelResource.DeattachDuration, - GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching"); + GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); #endif FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false); return true; @@ -817,12 +817,12 @@ namespace Barotrauma.Items.Components if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f)) - { - character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leaksfixed", 10.0f); + { + character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leaksfixed".ToIdentifier(), 10.0f); } else { - character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leakfixed", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leakfixed".ToIdentifier(), 10.0f); } } @@ -882,7 +882,7 @@ namespace Barotrauma.Items.Components if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } for (int i = 0; i < effect.propertyNames.Length; i++) { - string propertyName = effect.propertyNames[i]; + Identifier propertyName = effect.propertyNames[i]; if (propertyName != "stuck") { continue; } if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } object value = property.GetValue(target); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs index 32a800d50..cbf773a34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs @@ -6,30 +6,30 @@ namespace Barotrauma.Items.Components { partial class Sprayer : RangedWeapon { - [Serialize(0.0f, false, description: "The distance at which the item can spray walls.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The distance at which the item can spray walls.")] public float Range { get; set; } - [Serialize(1.0f, false, description: "How fast the item changes the color of the walls.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the item changes the color of the walls.")] public float SprayStrength { get; set; } - private readonly Dictionary liquidColors; + private readonly Dictionary liquidColors; private ItemContainer liquidContainer; - public Sprayer(Item item, XElement element) : base(item, element) + public Sprayer(Item item, ContentXElement element) : base(item, element) { item.IsShootable = true; item.RequireAimToUse = true; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "paintcolors": { - liquidColors = new Dictionary(); + liquidColors = new Dictionary(); foreach (XElement paintElement in subElement.Elements()) { - string paintName = paintElement.GetAttributeString("paintitem", string.Empty); + Identifier paintName = paintElement.GetAttributeIdentifier("paintitem", Identifier.Empty); Color paintColor = paintElement.GetAttributeColor("color", Color.Transparent); if (paintName != string.Empty) @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components liquidContainer = item.GetComponent(); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); #if SERVER public override bool Use(float deltaTime, Character character = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 66b0e88f0..3e9121f91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -21,10 +21,10 @@ namespace Barotrauma.Items.Components private set; } - [Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")] public float ThrowForce { get; set; } - public Throwable(Item item, XElement element) + public Throwable(Item item, ContentXElement element) : base(item, element) { //throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index e7ac5e34e..afe64e268 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.IO; #if CLIENT using Microsoft.Xna.Framework.Graphics; using Barotrauma.Sounds; @@ -64,26 +65,26 @@ namespace Barotrauma.Items.Components } } - public readonly XElement originalElement; + public readonly ContentXElement originalElement; protected const float CorrectionDelay = 1.0f; protected CoroutineHandle delayedCorrectionCoroutine; - [Editable, Serialize(0.0f, false, description: "How long it takes to pick up the item (in seconds).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime { get; set; } - [Serialize("", false, description: "What to display on the progress bar when this item is being picked.")] + [Serialize("", IsPropertySaveable.No, description: "What to display on the progress bar when this item is being picked.")] public string PickingMsg { get; set; } - public Dictionary SerializableProperties { get; protected set; } + public Dictionary SerializableProperties { get; protected set; } public Action OnActiveStateChanged; @@ -135,42 +136,42 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, false, description: "Can the item be picked up (or interacted with, if the pick action does something else than picking up the item).")] //Editable for doors to do their magic + [Editable, Serialize(false, IsPropertySaveable.No, description: "Can the item be picked up (or interacted with, if the pick action does something else than picking up the item).")] //Editable for doors to do their magic public bool CanBePicked { get { return canBePicked; } set { canBePicked = value; } } - [Serialize(false, false, description: "Should the interface of the item (if it has one) be drawn when the item is equipped.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the interface of the item (if it has one) be drawn when the item is equipped.")] public bool DrawHudWhenEquipped { get; protected set; } - [Serialize(false, false, description: "Can the item be selected by interacting with it.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")] public bool CanBeSelected { get { return canBeSelected; } set { canBeSelected = value; } } - [Serialize(false, false, description: "Can the item be combined with other items of the same type.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be combined with other items of the same type.")] public bool CanBeCombined { get { return canBeCombined; } set { canBeCombined = value; } } - [Serialize(false, false, description: "Should the item be removed if combining it with an other item causes the condition of this item to drop to 0.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be removed if combining it with an other item causes the condition of this item to drop to 0.")] public bool RemoveOnCombined { get { return removeOnCombined; } set { removeOnCombined = value; } } - [Serialize(false, false, description: "Can the \"Use\" action of the item be triggered by characters or just other items/StatusEffects.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the \"Use\" action of the item be triggered by characters or just other items/StatusEffects.")] public bool CharacterUsable { get { return characterUsable; } @@ -178,7 +179,7 @@ namespace Barotrauma.Items.Components } //Remove item if combination results in 0 condition - [Serialize(true, false, description: "Can the properties of the component be edited in-game (only applicable if the component has in-game editable properties)."), Editable()] + [Serialize(true, IsPropertySaveable.No, description: "Can the properties of the component be edited in-game (only applicable if the component has in-game editable properties)."), Editable()] public bool AllowInGameEditing { get; @@ -197,7 +198,7 @@ namespace Barotrauma.Items.Components protected set; } - [Serialize(false, false, description: "Should the item be deleted when it's used.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it's used.")] public bool DeleteOnUse { get; @@ -214,14 +215,14 @@ namespace Barotrauma.Items.Components get { return name; } } - [Editable, Serialize("", true, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's highlighted (generally instructs how to interact with the item, e.g. \"[Mouse1] Pick up\").")] + [Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's highlighted (generally instructs how to interact with the item, e.g. \"[Mouse1] Pick up\").")] public string Msg { get; set; } - public string DisplayMsg + public LocalizedString DisplayMsg { get; set; @@ -232,16 +233,16 @@ namespace Barotrauma.Items.Components /// /// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). /// - [Serialize(0f, false, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")] + [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")] public float CombatPriority { get; private set; } /// /// Which sound should be played when manual sound selection type is selected? Not [Editable] because we don't want this visible in the editor for every component. /// - [Serialize(0, true, alwaysUseInstanceValues: true)] + [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int ManuallySelectedSound { get; private set; } - public ItemComponent(Item item, XElement element) + public ItemComponent(Item item, ContentXElement element) { this.item = item; originalElement = element; @@ -321,7 +322,7 @@ namespace Barotrauma.Items.Components } } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -344,11 +345,11 @@ namespace Barotrauma.Items.Components case "requiredskills": if (subElement.Attribute("name") != null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); continue; } - string skillIdentifier = subElement.GetAttributeString("identifier", ""); + Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", ""); requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); break; case "statuseffect": @@ -357,7 +358,7 @@ namespace Barotrauma.Items.Components break; default: if (LoadElemProjSpecific(subElement)) { break; } - ItemComponent ic = Load(subElement, item, item.ConfigFile, false); + ItemComponent ic = Load(subElement, item, false); if (ic == null) { break; } ic.Parent = this; @@ -369,7 +370,7 @@ namespace Barotrauma.Items.Components } } - void LoadStatusEffect(XElement subElement) + void LoadStatusEffect(ContentXElement subElement) { var statusEffect = StatusEffect.Load(subElement, item.Name); if (!statusEffectLists.TryGetValue(statusEffect.type, out List effectList)) @@ -386,7 +387,7 @@ namespace Barotrauma.Items.Components IsActive = isActive; } - public void SetRequiredItems(XElement element) + public void SetRequiredItems(ContentXElement element) { bool returnEmpty = false; #if CLIENT @@ -410,7 +411,7 @@ namespace Barotrauma.Items.Components } else { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); } } @@ -517,7 +518,7 @@ namespace Barotrauma.Items.Components } item.ParentInventory.RemoveItem(item); } - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } else { @@ -533,7 +534,7 @@ namespace Barotrauma.Items.Components } this.Item.ParentInventory.RemoveItem(this.Item); } - Entity.Spawner.AddToRemoveQueue(this.Item); + Entity.Spawner.AddItemToRemoveQueue(this.Item); } else { @@ -609,6 +610,9 @@ namespace Barotrauma.Items.Components protected virtual void RemoveComponentSpecific() { } + + protected string GetTextureDirectory(ContentXElement subElement) + => subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty; public bool HasRequiredSkills(Character character) { @@ -676,7 +680,7 @@ namespace Barotrauma.Items.Components HasRequiredContainedItems(user, addMessage: false) && (!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); - public bool HasRequiredContainedItems(Character user, bool addMessage, string msg = null) + public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null) { if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; } if (item.OwnInventory == null) { return false; } @@ -686,8 +690,8 @@ namespace Barotrauma.Items.Components if (!ri.CheckRequirements(user, item)) { #if CLIENT - msg = msg ?? ri.Msg; - if (addMessage && !string.IsNullOrEmpty(msg)) + msg ??= ri.Msg; + if (addMessage && !msg.IsNullOrEmpty()) { GUI.AddMessage(msg, Color.Red); } @@ -720,7 +724,7 @@ namespace Barotrauma.Items.Components return false; } - public virtual bool HasRequiredItems(Character character, bool addMessage, string msg = null) + public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { if (requiredItems.None()) { return true; } if (character.Inventory == null) { return false; } @@ -746,7 +750,7 @@ namespace Barotrauma.Items.Components } #if CLIENT - if (!hasRequiredItems && addMessage && !string.IsNullOrEmpty(msg)) + if (!hasRequiredItems && addMessage && !msg.IsNullOrEmpty()) { GUI.AddMessage(msg, Color.Red); } @@ -802,7 +806,7 @@ namespace Barotrauma.Items.Components } if (!hasRequiredItems) { - if (msg == null && !string.IsNullOrEmpty(relatedItem.Msg)) + if (msg == null && !relatedItem.Msg.IsNullOrEmpty()) { msg = relatedItem.Msg; } @@ -850,13 +854,13 @@ namespace Barotrauma.Items.Components #endif } - public virtual void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { if (componentElement != null) { foreach (XAttribute attribute in componentElement.Attributes()) { - if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } + if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; } if (property.OverridePrefabValues || !usePrefabValues) { property.TrySetValue(this, attribute.Value); @@ -881,43 +885,48 @@ namespace Barotrauma.Items.Components public virtual void OnScaleChanged() { } - // TODO: Consider using generics, interfaces, or inheritance instead of reflection -> would be easier to debug when something changes/goes wrong. - // For example, currently we can edit the constructors but they will fail in runtime because the parameters are not changed here. - // It's also painful to find where the constructors are used, because the references exist only at runtime. - public static ItemComponent Load(XElement element, Item item, string file, bool errorMessages = true) + public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true) { - Type t; - string type = element.Name.ToString().ToLowerInvariant(); + Type type; + Identifier typeName = element.NameAsIdentifier(); try { - // Get the type of a specified class. - t = Type.GetType("Barotrauma.Items.Components." + type + "", false, true); - if (t == null) + // Get the type of a specified class. + type = ReflectionUtils.GetDerivedNonAbstract().Append(typeof(ItemComponent)).FirstOrDefault(t => t.Name == typeName); + if (type == null) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + file + ")"); + if (errorMessages) + { + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); + } return null; } } catch (Exception e) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + file + ")", e); + if (errorMessages) + { + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); + } return null; } ConstructorInfo constructor; try { - if (t != typeof(ItemComponent) && !t.IsSubclassOf(typeof(ItemComponent))) return null; - constructor = t.GetConstructor(new Type[] { typeof(Item), typeof(XElement) }); + if (type != typeof(ItemComponent) && !type.IsSubclassOf(typeof(ItemComponent))) { return null; } + constructor = type.GetConstructor(new Type[] { typeof(Item), typeof(ContentXElement) }); if (constructor == null) { - DebugConsole.ThrowError("Could not find the constructor of the component \"" + type + "\" (" + file + ")"); + DebugConsole.ThrowError( + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); return null; } } catch (Exception e) { - DebugConsole.ThrowError("Could not find the constructor of the component \"" + type + "\" (" + file + ")", e); + DebugConsole.ThrowError( + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); return null; } ItemComponent ic = null; @@ -930,10 +939,11 @@ namespace Barotrauma.Items.Components } catch (TargetInvocationException e) { - DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException); - GameAnalyticsManager.AddErrorEventOnce("ItemComponent.Load:TargetInvocationException" + item.Name + element.Name, + DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException); + GameAnalyticsManager.AddErrorEventOnce( + $"ItemComponent.Load:TargetInvocationException{item.Name}{element.Name}", GameAnalyticsManager.ErrorSeverity.Error, - "Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace.CleanupStackTrace()); + $"Error while loading entity of the type {type} ({e.InnerException})\n{Environment.StackTrace.CleanupStackTrace()}"); } return ic; @@ -974,7 +984,7 @@ namespace Barotrauma.Items.Components OverrideRequiredItems(originalElement); } - private void OverrideRequiredItems(XElement element) + private void OverrideRequiredItems(ContentXElement element) { var prevRequiredItems = new Dictionary>(requiredItems); requiredItems.Clear(); @@ -983,7 +993,7 @@ namespace Barotrauma.Items.Components #if CLIENT returnEmptyRequirements = Screen.Selected == GameMain.SubEditorScreen; #endif - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1014,8 +1024,8 @@ namespace Barotrauma.Items.Components public virtual void ParseMsg() { - string msg = TextManager.Get(Msg, true); - if (msg != null) + LocalizedString msg = TextManager.Get(Msg); + if (msg.Loaded) { msg = TextManager.ParseInputTypes(msg); DisplayMsg = msg; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index e32673000..40bd8a5b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components //how many items can be contained private int capacity; - [Serialize(5, false, description: "How many items can be contained inside this item.")] + [Serialize(5, IsPropertySaveable.No, description: "How many items can be contained inside this item.")] public int Capacity { get { return capacity; } @@ -66,7 +66,7 @@ namespace Barotrauma.Items.Components //how many items can be contained private int maxStackSize; - [Serialize(64, false, description: "How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")] + [Serialize(64, IsPropertySaveable.No, description: "How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")] public int MaxStackSize { get { return maxStackSize; } @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components } private bool hideItems; - [Serialize(true, false, description: "Should the items contained inside this item be hidden." + [Serialize(true, IsPropertySaveable.No, description: "Should the items contained inside this item be hidden." + " If set to false, you should use the ItemPos and ItemInterval properties to determine where the items get rendered.")] public bool HideItems { @@ -85,55 +85,55 @@ namespace Barotrauma.Items.Components Drawable = !hideItems; } } - - [Serialize("0.0,0.0", false, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")] + + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")] public Vector2 ItemPos { get; set; } - [Serialize("0.0,0.0", false, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] public Vector2 ItemInterval { get; set; } - [Serialize(100, false, description: "How many items are placed in a row before starting a new row.")] + [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } - [Serialize(true, false, description: "Should the inventory of this item be visible when the item is selected.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")] public bool DrawInventory { get; set; } - [Serialize(true, false, "Allow dragging and dropping items to deposit items into this inventory.")] + [Serialize(true, IsPropertySaveable.No, "Allow dragging and dropping items to deposit items into this inventory.")] public bool AllowDragAndDrop { get; set; } - - [Serialize(true, false)] + + [Serialize(true, IsPropertySaveable.No)] public bool AllowSwappingContainedItems { get; set; } - [Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] + [Serialize(false, IsPropertySaveable.No, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] public bool AutoInteractWithContained { get; set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool AllowAccess { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } - [Serialize(5, false, description: "How many inventory slots the inventory has per row.")] + [Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } private readonly HashSet containableRestrictions = new HashSet(); - [Editable, Serialize("", true, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")] + [Editable, Serialize("", IsPropertySaveable.Yes, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")] public string ContainableRestrictions { get { return string.Join(",", containableRestrictions); } @@ -143,46 +143,46 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, true, description: "Should this container be automatically filled with items?")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should this container be automatically filled with items?")] public bool AutoFill { get; set; } private float itemRotation; - [Serialize(0.0f, false, description: "The rotation in which the contained sprites are drawn (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The rotation in which the contained sprites are drawn (in degrees).")] public float ItemRotation { get { return MathHelper.ToDegrees(itemRotation); } set { itemRotation = MathHelper.ToRadians(value); } } - [Serialize("", false, description: "Specify an item for the container to spawn with.")] + [Serialize("", IsPropertySaveable.No, description: "Specify an item for the container to spawn with.")] public string SpawnWithId { get; set; } - [Serialize(false, false, description: "Should the items configured using SpawnWithId spawn if this item is broken.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the items configured using SpawnWithId spawn if this item is broken.")] public bool SpawnWithIdWhenBroken { get; set; } - - [Serialize(false, false, description: "Should the items be injected into the user.")] + + [Serialize(false, IsPropertySaveable.No, description: "Should the items be injected into the user.")] public bool AutoInject { get; set; } - [Serialize(0.5f, false, description: "The health threshold that the user must reach in order to activate the autoinjection.")] + [Serialize(0.5f, IsPropertySaveable.No, description: "The health threshold that the user must reach in order to activate the autoinjection.")] public float AutoInjectThreshold { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } private SlotRestrictions[] slotRestrictions; @@ -202,20 +202,20 @@ namespace Barotrauma.Items.Components if (!isRestrictionsDefined) { return true; } return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id)); } - - private ImmutableHashSet containableItemIdentifiers; - public IEnumerable ContainableItemIdentifiers => containableItemIdentifiers; + + private ImmutableHashSet containableItemIdentifiers; + public IEnumerable ContainableItemIdentifiers => containableItemIdentifiers; public override bool RecreateGUIOnResolutionChange => true; public List ContainableItems { get; } - public ItemContainer(Item item, XElement element) + public ItemContainer(Item item, ContentXElement element) : base(item, element) { int totalCapacity = capacity; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); continue; } ContainableItems ??= new List(); @@ -242,7 +242,7 @@ namespace Barotrauma.Items.Components } int subContainerIndex = capacity; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; } @@ -250,14 +250,14 @@ namespace Barotrauma.Items.Components int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); List subContainableItems = null; - foreach (XElement subSubElement in subElement.Elements()) + foreach (var subSubElement in subElement.Elements()) { if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; } RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); continue; } subContainableItems ??= new List(); @@ -283,7 +283,7 @@ namespace Barotrauma.Items.Components return slotRestrictions[slotIndex].MaxStackSize; } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public void OnItemContained(Item containedItem) { @@ -308,7 +308,7 @@ namespace Barotrauma.Items.Components if (item.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningPlanted:" + containedItem.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier); } //no need to Update() if this item has no statuseffects and no physics body @@ -424,7 +424,7 @@ namespace Barotrauma.Items.Components } } - public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } @@ -648,7 +648,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems; - containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty()).ToImmutableHashSet(); + containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty()).ToImmutableHashSet(); if (item.Submarine == null || !item.Submarine.Loading) { SpawnAlwaysContainedItems(); @@ -716,7 +716,7 @@ namespace Barotrauma.Items.Components else { IsActive = true; - Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false, onSpawned: (Item item) => { alwaysContainedItemsSpawned = true; }); + Entity.Spawner?.AddItemToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false, onSpawned: (Item item) => { alwaysContainedItemsSpawned = true; }); } } } @@ -745,7 +745,7 @@ namespace Barotrauma.Items.Components Inventory.AllItemsMod.ForEach(it => it.Drop(null)); } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs index 205db5844..c1cb4be89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs @@ -7,14 +7,14 @@ namespace Barotrauma.Items.Components { public static List List { get; } = new List(); - public Ladder(Item item, XElement element) + public Ladder(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); List.Add(this); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override bool Select(Character character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 77323447c..f4897de76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components public string Name => LimbType.ToString(); - public Dictionary SerializableProperties => null; + public Dictionary SerializableProperties => null; public LimbPos(LimbType limbType, Vector2 position, bool allowUsingLimb) { @@ -61,21 +61,21 @@ namespace Barotrauma.Items.Components public IEnumerable LimbPositions { get { return limbPositions; } } - [Editable, Serialize(false, false, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)] public bool IsToggle { get; set; } - [Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] public bool State { get; set; } - [Serialize(true, false, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")] public bool HideHUD { get; @@ -87,10 +87,10 @@ namespace Barotrauma.Items.Components Air, Water, Both }; - [Serialize(UseEnvironment.Both, false, description: "Can the item be selected in air, underwater or both.")] + [Serialize(UseEnvironment.Both, IsPropertySaveable.No, description: "Can the item be selected in air, underwater or both.")] public UseEnvironment UsableIn { get; set; } - [Serialize(false, false, description: "Should the character using the item be drawn behind the item.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the character using the item be drawn behind the item.")] public bool DrawUserBehind { get; @@ -114,7 +114,7 @@ namespace Barotrauma.Items.Components private set; } = true; - public Controller(Item item, XElement element) + public Controller(Item item, ContentXElement element) : base(item, element) { limbPositions = new List(); @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components Enum.TryParse(element.GetAttributeString("direction", "None"), out dir); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name != "limbposition") { continue; } string limbStr = subElement.GetAttributeString("limb", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 944fd6055..004d78dd2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -33,15 +33,15 @@ namespace Barotrauma.Items.Components get { return outputContainer; } } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DeconstructItemsSimultaneously { get; set; } - [Editable, Serialize(1.0f, true)] + [Editable, Serialize(1.0f, IsPropertySaveable.Yes)] public float DeconstructionSpeed { get; set; } public override bool RecreateGUIOnResolutionChange => true; - public Deconstructor(Item item, XElement element) + public Deconstructor(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); @@ -88,8 +88,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerConsumption <= 0.0f) { Voltage = 1.0f; } - progressTimer += deltaTime * Math.Min(Voltage, 1.0f); + progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, 1.0f); float tinkeringStrength = 0f; if (repairable.IsTinkering) @@ -114,9 +113,9 @@ namespace Barotrauma.Items.Components foreach (Item targetItem in items) { if ((Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false) || !inputContainer.Inventory.AllItems.Contains(targetItem)) { continue; } - var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it => - (it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) && - (it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it != targetItem && (it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))))); + var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => + (it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) && + (it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it != targetItem && (it.HasTag(r) || it.Prefab.Identifier == r))))).ToList(); ProcessItem(targetItem, items, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any()); } @@ -133,8 +132,8 @@ namespace Barotrauma.Items.Components var targetItem = inputContainer.Inventory.LastOrDefault(); if (targetItem == null) { return; } - var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it => - it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))); + var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => + it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)).ToList(); float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; @@ -230,7 +229,7 @@ namespace Barotrauma.Items.Components foreach (Item otherItem in inputItems) { if (targetItem == otherItem) { continue; } - if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) + if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier)) { user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); foreach (Character character in Character.GetFriendlyCrew(user)) @@ -246,14 +245,14 @@ namespace Barotrauma.Items.Components { inputContainer.Inventory.RemoveItem(otherItem); OutputContainer.Inventory.RemoveItem(otherItem); - Entity.Spawner.AddToRemoveQueue(otherItem); + Entity.Spawner.AddItemToRemoveQueue(otherItem); } allowRemove = false; return; } inputContainer.Inventory.RemoveItem(otherItem); OutputContainer.Inventory.RemoveItem(otherItem); - Entity.Spawner.AddToRemoveQueue(otherItem); + Entity.Spawner.AddItemToRemoveQueue(otherItem); } } } @@ -268,7 +267,7 @@ namespace Barotrauma.Items.Components int amount = (int)amountMultiplier; for (int i = 0; i < amount; i++) { - Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { spawnedItem.StolenDuringRound = targetItem.StolenDuringRound; spawnedItem.AllowStealing = targetItem.AllowStealing; @@ -300,7 +299,7 @@ namespace Barotrauma.Items.Components } } - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + targetItem.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier); if (targetItem.AllowDeconstruct && allowRemove) { @@ -314,7 +313,7 @@ namespace Barotrauma.Items.Components } } inputContainer.Inventory.RemoveItem(targetItem); - Entity.Spawner.AddToRemoveQueue(targetItem); + Entity.Spawner.AddItemToRemoveQueue(targetItem); MoveInputQueue(); PutItemsToLinkedContainer(); } @@ -392,16 +391,16 @@ namespace Barotrauma.Items.Components { if (deconstructItem.RequiredDeconstructor.Length > 0) { - if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) { continue; } } if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems) { - if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))) { continue; } + if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier == r))) { continue; } bool validOtherItemFound = false; foreach (Item otherInputItem in items) { if (otherInputItem == inputItem) { continue; } - if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier == r)) { continue; } var geneticMaterial1 = inputItem.GetComponent(); var geneticMaterial2 = otherInputItem.GetComponent(); @@ -427,7 +426,7 @@ namespace Barotrauma.Items.Components if (inputContainer.Inventory.IsEmpty()) { active = false; } IsActive = active; - currPowerConsumption = IsActive ? powerConsumption : 0.0f; + //currPowerConsumption = IsActive ? powerConsumption : 0.0f; userDeconstructorSpeedMultiplier = user != null ? 1f + user.GetStatValue(StatTypes.DeconstructorSpeedMultiplier) : 1f; #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 177571933..ceab83e9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components public Character User; [Editable(0.0f, 10000000.0f), - Serialize(500.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] + Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { get { return maxForce; } @@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize("0.0,0.0", true, + [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The position of the propeller as an offset from the item's center (in pixels)."+ " Determines where the particles spawn and the position that causes characters to take damage from the engine if the PropellerDamage is defined.")] public Vector2 PropellerPos @@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DisablePropellerDamage { get; @@ -75,12 +75,12 @@ namespace Barotrauma.Items.Components private const float TinkeringForceIncrease = 1.5f; - public Engine(Item item, XElement element) + public Engine(Item item, ContentXElement element) : base(item, element) { IsActive = true; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -93,7 +93,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -103,14 +103,16 @@ namespace Barotrauma.Items.Components controlLockTimer -= deltaTime; - currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; - //engines consume more power when in a bad condition - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + if (powerConsumption == 0.0f) + { + prevVoltage = 1; + hasPower = true; + } + else + { + hasPower = Voltage > MinVoltage; + } - if (powerConsumption == 0.0f) { Voltage = 1.0f; } - - prevVoltage = Voltage; - hasPower = Voltage > MinVoltage; Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f); if (Math.Abs(Force) > 1.0f) @@ -154,6 +156,33 @@ namespace Barotrauma.Items.Components } } + /// + /// Power consumption of the engine. Only consume power when active and adjust consumption based on condition and target force. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != this.powerIn) + { + return 0; + } + + currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; + //engines consume more power when in a bad condition + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + return currPowerConsumption; + } + + /// + /// When grid is resolved update the previous voltage + /// + public override void GridResolved(Connection connection) + { + if (connection == powerIn) + { + prevVoltage = Voltage; + } + } + private void UpdateAITargets(float noise) { if (item.AiTarget != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 43dfd81d0..4fc2c9cf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -1,7 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -11,7 +13,7 @@ namespace Barotrauma.Items.Components { partial class Fabricator : Powered, IServerSerializable, IClientSerializable { - private readonly List fabricationRecipes = new List(); + private ImmutableDictionary fabricationRecipes; //this is not readonly because tutorials fuck this up!!!! private FabricationRecipe fabricatedItem; private float timeUntilReady; @@ -20,7 +22,7 @@ namespace Barotrauma.Items.Components private string savedFabricatedItem; private float savedTimeUntilReady, savedRequiredTime; - private readonly Dictionary> availableIngredients = new Dictionary>(); + private readonly Dictionary> availableIngredients = new Dictionary>(); const float RefreshIngredientsInterval = 1.0f; private float refreshIngredientsTimer; @@ -31,10 +33,10 @@ namespace Barotrauma.Items.Components private ItemContainer inputContainer, outputContainer; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float FabricationSpeed { get; set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float SkillRequirementMultiplier { get; set; } private const float TinkeringSpeedIncrease = 2.5f; @@ -78,10 +80,10 @@ namespace Barotrauma.Items.Components private float progressState; - public Fabricator(Item item, XElement element) + public Fabricator(Item item, ContentXElement element) : base(item, element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase)) { @@ -90,26 +92,22 @@ namespace Barotrauma.Items.Components } } + var fabricationRecipes = new Dictionary(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes) + foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes.Values) { if (recipe.SuitableFabricatorIdentifiers.Length > 0) { - if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.prefab.Identifier == i || item.HasTag(i))) + if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.Prefab.Identifier == i || item.HasTag(i))) { continue; } } - fabricationRecipes.Add(recipe); + fabricationRecipes.Add(recipe.RecipeHash, recipe); } } - fabricationRecipes.Sort((r1, r2) => - { - int hash1 = (int)r1.TargetItem.UIntIdentifier; - int hash2 = (int)r2.TargetItem.UIntIdentifier; - return hash1 - hash2; - }); + this.fabricationRecipes = fabricationRecipes.ToImmutableDictionary(); state = FabricatorState.Stopped; @@ -129,9 +127,9 @@ namespace Barotrauma.Items.Components inputContainer = containers[0]; outputContainer = containers[1]; - foreach (var recipe in fabricationRecipes) + foreach (var recipe in fabricationRecipes.Values) { - if (recipe.RequiredItems.Count > inputContainer.Capacity) + if (recipe.RequiredItems.Length > inputContainer.Capacity) { DebugConsole.ThrowError("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!"); } @@ -158,16 +156,11 @@ namespace Barotrauma.Items.Components return picker != null; } - public void RemoveFabricationRecipes(List allowedIdentifiers) + public void RemoveFabricationRecipes(IEnumerable allowedIdentifiers) { - for (int i = 0; i < fabricationRecipes.Count; i++) - { - if (!allowedIdentifiers.Contains(fabricationRecipes[i].TargetItem.Identifier)) - { - fabricationRecipes.RemoveAt(i); - i--; - } - } + fabricationRecipes = fabricationRecipes + .Where(kvp => allowedIdentifiers.Contains(kvp.Value.TargetItemPrefabIdentifier)) + .ToImmutableDictionary(); CreateRecipes(); } @@ -201,9 +194,6 @@ namespace Barotrauma.Items.Components inputContainer.Inventory.Locked = true; outputContainer.Inventory.Locked = true; - currPowerConsumption = powerConsumption; - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); - if (GameMain.NetworkMember?.IsServer ?? true) { State = FabricatorState.Active; @@ -211,7 +201,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null && addToServerLog) { - GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName.Value + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } @@ -237,7 +227,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName.Value + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #elif CLIENT itemList.Enabled = true; @@ -304,11 +294,10 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerConsumption <= 0) { Voltage = 1.0f; } float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; - timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f); + timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, 1.0f); UpdateRequiredTimeProjSpecific(); @@ -370,7 +359,7 @@ namespace Barotrauma.Items.Components } availableItems.Remove(availableItem); - Entity.Spawner.AddToRemoveQueue(availableItem); + Entity.Spawner.AddItemToRemoveQueue(availableItem); inputContainer.Inventory.RemoveItem(availableItem); break; } @@ -389,7 +378,7 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); - + quality = GetFabricatedItemQuality(fabricatedItem, user); } @@ -397,10 +386,10 @@ namespace Barotrauma.Items.Components for (int i = 0; i < (int)fabricationitemAmount.Value; i++) { float outCondition = fabricatedItem.OutCondition; - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); if (i < amountFittingContainer) { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, + Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); @@ -413,7 +402,7 @@ namespace Barotrauma.Items.Components } else { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality, + Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); @@ -469,13 +458,30 @@ namespace Barotrauma.Items.Components } + /// + /// Power consumption of the fabricator. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + //No consumption if not powerin or is off + if (connection != this.powerIn || !IsActive) + { + return 0; + } + + currPowerConsumption = PowerConsumption; + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + + return currPowerConsumption; + } + private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { if (user == null) { return 0; } if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } int quality = 0; float floatQuality = 0.0f; - foreach (string tag in fabricatedItem.TargetItem.Tags) + foreach (var tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); } @@ -489,8 +495,8 @@ namespace Barotrauma.Items.Components } partial void UpdateRequiredTimeProjSpecific(); - - private bool CanBeFabricated(FabricationRecipe fabricableItem, Dictionary> availableIngredients, Character character) + + private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } @@ -533,13 +539,13 @@ namespace Barotrauma.Items.Components return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f); } - public float FabricationDegreeOfSuccess(Character character, List skills) + public float FabricationDegreeOfSuccess(Character character, ImmutableArray skills) { - if (skills.Count == 0) { return 1.0f; } + if (skills.Length == 0) { return 1.0f; } if (character == null) { return 0.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); - float average = skillSum / skills.Count; + float average = skillSum / skills.Length; return (average + 100.0f) / 2.0f / 100.0f; } @@ -593,7 +599,7 @@ namespace Barotrauma.Items.Components availableIngredients.Clear(); foreach (Item item in itemList) { - var itemIdentifier = item.prefab.Identifier; + var itemIdentifier = item.Prefab.Identifier; if (!availableIngredients.ContainsKey(itemIdentifier)) { availableIngredients[itemIdentifier] = new List(itemList.Count); @@ -663,7 +669,7 @@ namespace Barotrauma.Items.Components return componentElement; } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", ""); @@ -678,7 +684,7 @@ namespace Barotrauma.Items.Components inputContainer?.OnMapLoaded(); outputContainer?.OnMapLoaded(); - var recipe = fabricationRecipes.Find(r => r.TargetItem.Identifier == savedFabricatedItem); + var recipe = fabricationRecipes.Values.FirstOrDefault(r => r.TargetItem.Identifier == savedFabricatedItem); if (recipe == null) { DebugConsole.ThrowError("Error while loading a fabricator. Can't continue fabricating \"" + savedFabricatedItem + "\" (matching recipe not found)."); @@ -696,13 +702,13 @@ namespace Barotrauma.Items.Components } class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier { - public AbilityFabricatorSkillGain(string skillIdentifier, float skillAmount) + public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount) { SkillIdentifier = skillIdentifier; Value = skillAmount; } public float Value { get; set; } - public string SkillIdentifier { get; set; } + public Identifier SkillIdentifier { get; set; } } class AbilityFabricationItemAmount : AbilityObject, IAbilityValue, IAbilityItemPrefab diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index d89269827..889969115 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -29,49 +29,49 @@ namespace Barotrauma.Items.Components private readonly Dictionary hullDatas; - [Editable, Serialize(false, true, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")] public bool RequireWaterDetectors { get; set; } - [Editable, Serialize(true, true, description: "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms.")] public bool RequireOxygenDetectors { get; set; } - [Editable, Serialize(true, true, description: "Should damaged walls be displayed by the machine.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should damaged walls be displayed by the machine.")] public bool ShowHullIntegrity { get; set; } - [Editable, Serialize(true, true, description: "Enable hull status mode.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Enable hull status mode.")] public bool EnableHullStatus { get; set; } - [Editable, Serialize(true, true, description: "Enable electrical view mode.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Enable electrical view mode.")] public bool EnableElectricalView { get; set; } - [Editable, Serialize(true, true, description: "Enable item finder mode.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Enable item finder mode.")] public bool EnableItemFinder { get; set; } - public MiniMap(Item item, XElement element) + public MiniMap(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -114,9 +114,6 @@ namespace Barotrauma.Items.Components } #endif - currPowerConsumption = powerConsumption; - currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); - hasPower = Voltage > MinVoltage; if (hasPower) { @@ -124,6 +121,19 @@ namespace Barotrauma.Items.Components } } + /// + /// Power consumption of the MiniMap. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != powerIn || !IsActive) + { + return 0; + } + + return PowerConsumption * MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + } + public override bool Pick(Character picker) { return picker != null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs index 0143e4013..a9439661a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { partial class OutpostTerminal : ItemComponent { - public OutpostTerminal(Item item, XElement element) : base(item, element) + public OutpostTerminal(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index d76ec970c..304a06991 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -24,14 +24,14 @@ namespace Barotrauma.Items.Components private set; } - [Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.", alwaysUseInstanceValues: true)] + [Editable, Serialize(400.0f, IsPropertySaveable.Yes, description: "How much oxygen the machine generates when operating at full power.", alwaysUseInstanceValues: true)] public float GeneratedAmount { get { return generatedAmount; } set { generatedAmount = MathHelper.Clamp(value, -10000.0f, 10000.0f); } } - public OxygenGenerator(Item item, XElement element) + public OxygenGenerator(Item item, ContentXElement element) : base(item, element) { //randomize update timer so all oxygen generators don't update at the same time @@ -44,25 +44,15 @@ namespace Barotrauma.Items.Components UpdateOnActiveEffects(deltaTime); CurrFlow = 0.0f; - currPowerConsumption = powerConsumption; - //consume more power when in a bad condition - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); - - if (powerConsumption <= 0.0f) - { - Voltage = 1.0f; - } if (item.CurrentHull == null) { return; } - - if (Voltage < MinVoltage) + + if (Voltage < MinVoltage && PowerConsumption > 0) { return; } - - CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f; - //less effective when in bad condition + CurrFlow = Math.Min(PowerConsumption > 0 ? Voltage : 1.0f, 1.0f) * generatedAmount * 100.0f; float conditionMult = item.Condition / item.MaxCondition; //100% condition = 100% oxygen //50% condition = 25% oxygen @@ -72,6 +62,23 @@ namespace Barotrauma.Items.Components UpdateVents(CurrFlow, deltaTime); } + /// + /// Power consumption of the Oxygen Generator. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != this.powerIn) + { + return 0; + } + + float consumption = powerConsumption; + + //consume more power when in a bad condition + item.GetComponent()?.AdjustPowerConsumption(ref consumption); + return consumption; + } + public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index b7cb7b076..37dbc543f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components private float pumpSpeedLockTimer, isActiveLockTimer; - [Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] public float FlowPercentage { get { return flowPercentage; } @@ -48,18 +48,18 @@ namespace Barotrauma.Items.Components { if (!MathUtils.IsValid(flowPercentage)) { return; } flowPercentage = MathHelper.Clamp(value, -100.0f, 100.0f); - flowPercentage = MathUtils.Round(flowPercentage, 1.0f); + flowPercentage = MathF.Round(flowPercentage); } } - [Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] + [Editable, Serialize(80.0f, IsPropertySaveable.No, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] public float MaxFlow { get { return maxFlow; } set { maxFlow = value; } } - [Editable, Serialize(true, true, alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool IsOn { get { return IsActive; } @@ -83,13 +83,13 @@ namespace Barotrauma.Items.Components public override bool UpdateWhenInactive => true; - public Pump(Item item, XElement element) + public Pump(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -120,10 +120,6 @@ namespace Barotrauma.Items.Components FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } - currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); - //pumps consume more power when in a bad condition - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); - if (!HasPower) { return; } UpdateProjSpecific(deltaTime); @@ -147,10 +143,9 @@ namespace Barotrauma.Items.Components item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate; if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 30.0f * deltaTime; } - Voltage -= deltaTime; } - public void InfectBallast(string identifier, bool allowMultiplePerShip = false) + public void InfectBallast(Identifier identifier, bool allowMultiplePerShip = false) { Hull hull = item.CurrentHull; if (hull == null) { return; } @@ -158,7 +153,7 @@ namespace Barotrauma.Items.Components if (!allowMultiplePerShip) { // if the ship is already infected then do nothing - if (Hull.hullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; } + if (Hull.HullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; } } if (hull.BallastFlora != null) { return; } @@ -178,6 +173,24 @@ namespace Barotrauma.Items.Components #endif } + /// + /// Power consumption of the Pump. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + //There shouldn't be other power connections to this + if (connection != this.powerIn) + { + return 0; + } + + currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); + //pumps consume more power when in a bad condition + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + + return currPowerConsumption; + } + partial void UpdateProjSpecific(float deltaTime); public override void ReceiveSignal(Signal signal, Connection connection) @@ -218,7 +231,8 @@ namespace Barotrauma.Items.Components #if CLIENT if (GameMain.Client != null) { return false; } #endif - switch (objective.Option.ToLowerInvariant()) + + switch (objective.Option.Value.ToLowerInvariant()) { case "pumpout": #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 02028708a..7abf1b323 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -36,8 +36,8 @@ namespace Barotrauma.Items.Components private float fireTimer, fireDelay; private float maxPowerOutput; - - private readonly Queue loadQueue = new Queue(); + private float minUpdatePowerOut; + private float maxUpdatePowerOut; private bool unsentChanges; private float sendUpdateTimer; @@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components private bool _powerOn; - [Serialize(defaultValue: false, isSaveable: true)] + [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] public bool PowerOn { get { return _powerOn; } @@ -63,9 +63,11 @@ namespace Barotrauma.Items.Components } } + protected override PowerPriority Priority { get { return PowerPriority.Reactor; } } + public Character LastAIUser { get; private set; } - [Serialize(defaultValue: false, isSaveable: true)] + [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] public bool LastUserWasPlayer { get; private set; } private Character lastUser; @@ -81,7 +83,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] public float MaxPowerOutput { get { return maxPowerOutput; } @@ -91,21 +93,21 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue), Serialize(120.0f, true, description: "How long the temperature has to stay critical until a meltdown occurs.")] + [Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")] public float MeltdownDelay { get { return meltDownDelay; } set { meltDownDelay = Math.Max(value, 0.0f); } } - [Editable(0.0f, float.MaxValue), Serialize(30.0f, true, description: "How long the temperature has to stay critical until the reactor catches fire.")] + [Editable(0.0f, float.MaxValue), Serialize(30.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until the reactor catches fire.")] public float FireDelay { get { return fireDelay; } set { fireDelay = Math.Max(value, 0.0f); } } - [Serialize(0.0f, true, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")] public float Temperature { get { return temperature; } @@ -116,7 +118,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true, description: "Current fission rate of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Current fission rate of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float FissionRate { get { return fissionRate; } @@ -127,7 +129,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true, description: "Current turbine output of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Current turbine output of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float TurbineOutput { get { return turbineOutput; } @@ -138,7 +140,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] + [Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] public float FuelConsumptionRate { get { return fuelConsumptionRate; } @@ -149,14 +151,14 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, true, description: "Is the temperature currently critical. Intended to be used by StatusEffect conditionals (setting the value from XML has no effect).")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the temperature currently critical. Intended to be used by StatusEffect conditionals (setting the value from XML has no effect).")] public bool TemperatureCritical { get { return temperature > allowedTemperature.Y; } set { /*do nothing*/ } } - [Serialize(false, true, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public bool AutoTemp { get { return autoTemp; } @@ -171,36 +173,36 @@ namespace Barotrauma.Items.Components private float prevAvailableFuel; - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float AvailableFuel { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public new float Load { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float TargetFissionRate { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float TargetTurbineOutput { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float CorrectTurbineOutput { get; set; } - [Editable, Serialize(true, true)] + [Editable, Serialize(true, IsPropertySaveable.Yes)] public bool ExplosionDamagesOtherSubs { get; set; } - public Reactor(Item item, XElement element) + public Reactor(Item item, ContentXElement element) : base(item, element) { IsActive = true; InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -248,7 +250,7 @@ namespace Barotrauma.Items.Components //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f)) { - CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -10.0f, 10.0f) * deltaTime; + CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; } //calculate tolerances of the meters based on the skills of the user @@ -277,25 +279,6 @@ namespace Barotrauma.Items.Components TurbineOutput = MathHelper.Lerp(turbineOutput, TargetTurbineOutput, deltaTime); float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); - currPowerConsumption = -MaxPowerOutput * Math.Min(turbineOutput / 100.0f, temperatureFactor); - - //if the turbine output and coolant flow are the optimal range, - //make the generated power slightly adjust according to the load - // (-> the reactor can automatically handle small changes in load as long as the values are roughly correct) - if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y && - temperature > optimalTemperature.X && temperature < optimalTemperature.Y) - { - float maxAutoAdjust = maxPowerOutput * 0.1f; - autoAdjustAmount = MathHelper.Lerp( - autoAdjustAmount, - MathHelper.Clamp(-Load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust), - deltaTime * 10.0f); - } - else - { - autoAdjustAmount = MathHelper.Lerp(autoAdjustAmount, 0.0f, deltaTime * 10.0f); - } - currPowerConsumption += autoAdjustAmount; if (!PowerOn) { @@ -304,37 +287,9 @@ namespace Barotrauma.Items.Components } else if (autoTemp) { - UpdateAutoTemp(2.0f, deltaTime); - } - float currentLoad = 0.0f; - List connections = item.Connections; - if (connections != null && connections.Count > 0) - { - foreach (Connection connection in connections) - { - if (!connection.IsPower) { continue; } - foreach (Connection recipient in connection.Recipients) - { - if (!(recipient.Item is Item it)) { continue; } - - PowerTransfer pt = it.GetComponent(); - if (pt == null) { continue; } - - //calculate how much external power there is in the grid - //(power coming from somewhere else than this reactor, e.g. batteries) - float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0) * 0.95f; - //reduce the external power from the load to prevent overloading the grid - currentLoad = Math.Max(currentLoad, pt.PowerLoad - externalPower); - } - } + UpdateAutoTemp(10.0f, deltaTime * 2f); } - loadQueue.Enqueue(currentLoad); - while (loadQueue.Count() > 60.0f) - { - Load = loadQueue.Average(); - loadQueue.Dequeue(); - } float fuelLeft = 0.0f; var containedItems = item.OwnInventory?.AllItems; @@ -405,6 +360,77 @@ namespace Barotrauma.Items.Components } } + /// + /// Returns a negative value (indicating the reactor generates power) when querying the power output connection. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + return connection != null && connection.IsPower && connection.IsOutput ? -1 : 0; + } + + /// + /// Min and Max power output of the reactor based on tolerance + /// + public override PowerRange MinMaxPowerOut(Connection conn, float load) + { + float tolerance = 1f; + + //If within the optimal output allow for slight output adjustments + if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y && + temperature > optimalTemperature.X && temperature < optimalTemperature.Y) + { + tolerance = 3f; + } + + float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); + float minOutput = MaxPowerOutput * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); + float maxOutput = MaxPowerOutput * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); + + minUpdatePowerOut = minOutput; + maxUpdatePowerOut = maxOutput; + + float reactorMax = PowerOn ? MaxPowerOutput : maxUpdatePowerOut; + + return new PowerRange(minOutput, maxOutput, reactorMax); + } + + /// + /// Determine how much power to output based on the load. The load is divided between reactors according to their maximum output in multi-reactor setups. + /// + public override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load) + { + //Load must be calculated at this stage instead of at gridResolved to remove influence of lower priority devices + float loadLeft = MathHelper.Max(load - power,0); + float expectedPower = MathHelper.Clamp(loadLeft, minMaxPower.Min, minMaxPower.Max); + + //Delta ratio of Min and Max power output capability of the grid + float ratio = MathHelper.Max((loadLeft - minMaxPower.Min) / (minMaxPower.Max - minMaxPower.Min), 0); + if (float.IsInfinity(ratio)) + { + ratio = 0; + } + + float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut); + float newLoad = loadLeft; + + //Adjust behaviour for multi reactor setup + if (MaxPowerOutput != minMaxPower.ReactorMaxOutput) + { + float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft; + float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100); + newLoad = MathHelper.Clamp(loadLeft - (expectedPower + output) + loadAdjust, 0, loadLeft); + } + + if (float.IsNegative(newLoad)) + { + newLoad = 0.0f; + } + + Load = newLoad; + currPowerConsumption = -output; + return output; + } + private float GetGeneratedHeat(float fissionRate) { return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f; @@ -595,7 +621,7 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } character.AIController.SteeringManager.Reset(); - bool shutDown = objective.Option.Equals("shutdown", StringComparison.OrdinalIgnoreCase); + bool shutDown = objective.Option == "shutdown"; IsActive = true; @@ -624,7 +650,7 @@ namespace Barotrauma.Items.Components var containObjective = AIContainItems(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true); containObjective.Completed += ReportFuelRodCount; containObjective.Abandoned += ReportFuelRodCount; - character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); + character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, "reactorfuel".ToIdentifier(), 30.0f); void ReportFuelRodCount() { @@ -633,12 +659,12 @@ namespace Barotrauma.Items.Components int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1); if (remainingFuelRods == 0) { - character.Speak(TextManager.Get("DialogOutOfFuelRods"), null, 0.0f, "outoffuelrods", 30.0f); + character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f); outOfFuel = true; } else if (remainingFuelRods < 3) { - character.Speak(TextManager.Get("DialogLowOnFuelRods"), null, 0.0f, "lowonfuelrods", 30.0f); + character.Speak(TextManager.Get("DialogLowOnFuelRods").Value, null, 0.0f, "lowonfuelrods".ToIdentifier(), 30.0f); } } } @@ -655,7 +681,7 @@ namespace Barotrauma.Items.Components } else { - character.Speak(TextManager.Get("DialogReactorIsBroken"), identifier: "reactorisbroken", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get("DialogReactorIsBroken").Value, identifier: "reactorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } } if (TooMuchFuel()) @@ -676,7 +702,7 @@ namespace Barotrauma.Items.Components { if (lastUser.SelectedConstruction == item && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f); + character.Speak(TextManager.Get("DialogReactorTaken").Value, null, 0.0f, "reactortaken".ToIdentifier(), 10.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index ec4fd008e..97657bc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components get { return connectedTransducers.Select(t => t.Transducer); } } - [Serialize(DefaultSonarRange, false, description: "The maximum range of the sonar.")] + [Serialize(DefaultSonarRange, IsPropertySaveable.No, description: "The maximum range of the sonar.")] public float Range { get { return range; } @@ -92,29 +92,29 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "Should the sonar display the walls of the submarine it is inside.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the sonar display the walls of the submarine it is inside.")] public bool DetectSubmarineWalls { get; set; } - [Editable, Serialize(false, false, description: "Does the sonar have to be connected to external transducers to work.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have to be connected to external transducers to work.")] public bool UseTransducers { get; set; } - [Editable, Serialize(false, false, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")] public bool CenterOnTransducers { get; set; } - [Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode. " + - "Only available in-game when the Item has no Steering component.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have mineral scanning mode. " + + "Only available in-game when the Item has no Steering component.")] public bool HasMineralScanner { get; set; } public float Zoom @@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; - public Sonar(Item item, XElement element) + public Sonar(Item item, ContentXElement element) : base(item, element) { connectedTransducers = new List(); @@ -155,12 +155,10 @@ namespace Barotrauma.Items.Components CurrentMode = Mode.Passive; } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { - currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption; - UpdateOnActiveEffects(deltaTime); if (UseTransducers) @@ -240,8 +238,19 @@ namespace Barotrauma.Items.Components ++pingIndex; } } + } - Voltage -= deltaTime; + /// + /// Power consumption of the sonar. Only consume power when active and adjust the consumption based on the sonar mode. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != powerIn || !IsActive) + { + return 0; + } + + return (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption; } public override bool Use(float deltaTime, Character character = null) @@ -266,7 +275,8 @@ namespace Barotrauma.Items.Components if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) { continue; } - string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition); + #warning This is not the best key for a dictionary. + string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value; if (!targetGroups.ContainsKey(directionName)) { targetGroups.Add(directionName, new List()); @@ -289,9 +299,10 @@ namespace Barotrauma.Items.Components if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" }, - new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() }, - new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60); + character.Speak(TextManager.GetWithVariables(dialogTag, + ("[direction]", targetGroup.Key.ToString(), FormatCapitals.Yes), + ("[count]", targetGroup.Value.Count.ToString(), FormatCapitals.No)).Value, + null, 0, $"sonartarget{targetGroup.Value[0].ID}".ToIdentifier(), 60); } //prevent the character from reporting other targets in the group @@ -304,7 +315,7 @@ namespace Barotrauma.Items.Components return true; } - private string GetDirectionName(Vector2 dir) + private LocalizedString GetDirectionName(Vector2 dir) { float angle = MathUtils.WrapAngleTwoPi((float)-Math.Atan2(dir.Y, dir.X) + MathHelper.PiOver2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs index b4c9c7252..40d736be3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Items.Components public Sonar ConnectedSonar; - public SonarTransducer(Item item, XElement element) : base(item, element) + public SonarTransducer(Item item, ContentXElement element) : base(item, element) { IsActive = true; } @@ -19,8 +19,6 @@ namespace Barotrauma.Items.Components { UpdateOnActiveEffects(deltaTime); - CurrPowerConsumption = powerConsumption * (ConnectedSonar?.CurrentMode == Sonar.Mode.Active ? 1.0f : Sonar.PassivePowerConsumption); - if (Voltage >= MinVoltage) { sendSignalTimer += deltaTime; @@ -31,5 +29,15 @@ namespace Barotrauma.Items.Components } } } + + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != powerIn || !IsActive) + { + return 0; + } + + return PowerConsumption * (ConnectedSonar?.CurrentMode == Sonar.Mode.Active ? 1.0f : Sonar.PassivePowerConsumption); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index e56dcf7e9..b41f7c4bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -1,4 +1,4 @@ -using Barotrauma.Networking; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -106,7 +106,7 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 1.0f, decimals: 4), - Serialize(0.5f, true, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards." + Serialize(0.5f, IsPropertySaveable.Yes, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards." + " Can be used to compensate if the ballast tanks are too large/small relative to the size of the submarine.")] public float NeutralBallastLevel { @@ -117,7 +117,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(1000.0f, true, description: "How close the docking port has to be to another docking port for the docking mode to become active.")] + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How close the docking port has to be to another docking port for the docking mode to become active.")] public float DockingAssistThreshold { get; @@ -231,14 +231,14 @@ namespace Barotrauma.Items.Components } #endregion - public Steering(Item item, XElement element) + public Steering(Item item, ContentXElement element) : base(item, element) { IsActive = true; InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void OnItemLoaded() { @@ -292,8 +292,6 @@ namespace Barotrauma.Items.Components controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null; } - currPowerConsumption = powerConsumption; - if (Voltage < MinVoltage) { return; } if (user != null && user.Removed) @@ -405,7 +403,7 @@ namespace Barotrauma.Items.Components float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; user.Info.IncreaseSkillLevel( - "helm", + "helm".ToIdentifier(), SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime); } @@ -724,7 +722,7 @@ namespace Barotrauma.Items.Components { if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f); + character.Speak(TextManager.Get("DialogSteeringTaken").Value, null, 0.0f, "steeringtaken".ToIdentifier(), 10.0f); } } user = character; @@ -738,7 +736,7 @@ namespace Barotrauma.Items.Components } else { - character.Speak(TextManager.Get("DialogNavTerminalIsBroken"), identifier: "navterminalisbroken", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get("DialogNavTerminalIsBroken").Value, identifier: "navterminalisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } } @@ -748,64 +746,72 @@ namespace Barotrauma.Items.Components AutoPilot = true; } IncreaseSkillLevel(user, deltaTime); - switch (objective.Option.ToLowerInvariant()) + if (objective.Option == "maintainposition") { - case "maintainposition": - if (objective.Override) - { - SetMaintainPosition(); - } - break; - case "navigateback": - if (Level.IsLoadedOutpost) { break; } + if (objective.Override) + { + SetMaintainPosition(); + } + } + else if (!Level.IsLoadedOutpost) + { + if (objective.Option == "navigateback") + { if (DockingSources.Any(d => d.Docked)) { item.SendSignal("1", "toggle_docking"); } + if (objective.Override) { if (MaintainPos || LevelEndSelected || !LevelStartSelected || navigateTactically) { unsentChanges = true; } + SetDestinationLevelStart(); } - break; - case "navigatetodestination": - if (Level.IsLoadedOutpost) { break; } + } + else if (objective.Option == "navigatetodestination") + { if (DockingSources.Any(d => d.Docked)) { item.SendSignal("1", "toggle_docking"); } + if (objective.Override) { if (MaintainPos || !LevelEndSelected || LevelStartSelected || navigateTactically) { unsentChanges = true; } + SetDestinationLevelEnd(); } - break; - case "navigatetactical": - if (Level.IsLoadedOutpost) { break; } + } + else if (objective.Option == "navigatetactical") + { if (DockingSources.Any(d => d.Docked)) { item.SendSignal("1", "toggle_docking"); } + if (objective.Override) { if (MaintainPos || LevelEndSelected || LevelStartSelected || !navigateTactically) { unsentChanges = true; } + SetDestinationTactical(); } - break; + } } + sonar?.AIOperate(deltaTime, character, objective); if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("dialogicespirespottedsonar"), null, 0.0f, "icespirespottedsonar", 60.0f); + character.Speak(TextManager.Get("dialogicespirespottedsonar").Value, null, 0.0f, "icespirespottedsonar".ToIdentifier(), 60.0f); } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs index f4cb519e8..6d51ecdc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components set { oxygenFlow = Math.Max(value, 0.0f); } } - public Vent (Item item, XElement element) : base(item, element) { } + public Vent (Item item, ContentXElement element) : base(item, element) { } public override void Update(float deltaTime, Camera cam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs index 25eebb6a9..4e5fc3b5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs @@ -4,10 +4,10 @@ namespace Barotrauma.Items.Components { class NameTag : ItemComponent { - [InGameEditable(MaxLength = 32), Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)] + [InGameEditable(MaxLength = 32), Serialize("", IsPropertySaveable.No, description: "Name written on the tag.", alwaysUseInstanceValues: true)] public string WrittenName { get; set; } - public NameTag(Item item, XElement element) : base(item, element) + public NameTag(Item item, ContentXElement element) : base(item, element) { AllowInGameEditing = true; DrawHudWhenEquipped = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index f59bc100d..ae166ebf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components public Vector2 Offset; public float Size; - public PlantSlot(XElement element) + public PlantSlot(ContentXElement element) { Offset = element.GetAttributeVector2("offset", Vector2.Zero); Size = element.GetAttributeFloat("size", 0.5f); @@ -64,14 +64,14 @@ namespace Barotrauma.Items.Components private float fertilizer; - [Serialize(0f, true, "How much fertilizer the planter has.")] + [Serialize(0f, IsPropertySaveable.Yes, "How much fertilizer the planter has.")] public float Fertilizer { get => fertilizer; set => fertilizer = Math.Clamp(value, 0, FertilizerCapacity); } - [Serialize(100f, true, "How much fertilizer can the planter hold.")] + [Serialize(100f, IsPropertySaveable.Yes, "How much fertilizer can the planter hold.")] public float FertilizerCapacity { get; set; } public Growable?[] GrowableSeeds = new Growable?[0]; @@ -81,11 +81,11 @@ namespace Barotrauma.Items.Components private ItemContainer? container; private float growthTickTimer; - public Planter(Item item, XElement element) : base(item, element) + public Planter(Item item, ContentXElement element) : base(item, element) { canBePicked = true; SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -117,7 +117,7 @@ namespace Barotrauma.Items.Components GrowableSeeds = new Growable[container.Capacity]; } - public override bool HasRequiredItems(Character character, bool addMessage, string? msg = null) + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString? msg = null) { if (container?.Inventory == null) { return false; } @@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components if (!anyDecayed || seed.Decayed || seed.FullyGrown) { container?.Inventory.RemoveItem(seed.Item); - Entity.Spawner?.AddToRemoveQueue(seed.Item); + Entity.Spawner?.AddItemToRemoveQueue(seed.Item); GrowableSeeds[i] = null; ApplyStatusEffects(ActionType.OnPicked, 1.0f, character); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 576ab086d..3d1631663 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -13,8 +13,6 @@ namespace Barotrauma.Items.Components private float charge; - //private float rechargeVoltage; - //how fast the battery can be recharged private float maxRechargeSpeed; @@ -27,45 +25,52 @@ namespace Barotrauma.Items.Components protected Vector2 indicatorPosition, indicatorSize; protected bool isHorizontal; - + + protected override PowerPriority Priority { get { return PowerPriority.Battery; } } + + private float currPowerOutput; public float CurrPowerOutput { - get; - private set; + get { return currPowerOutput; } + private set + { + System.Diagnostics.Debug.Assert(value >= 0.0f); + currPowerOutput = Math.Max(0, value); + } } - [Serialize("0,0", true, description: "The position of the progress bar indicating the charge of the item. In pixels as an offset from the upper left corner of the sprite.")] + [Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the progress bar indicating the charge of the item. In pixels as an offset from the upper left corner of the sprite.")] public Vector2 IndicatorPosition { get { return indicatorPosition; } set { indicatorPosition = value; } } - [Serialize("0,0", true, description: "The size of the progress bar indicating the charge of the item (in pixels).")] + [Serialize("0,0", IsPropertySaveable.Yes, description: "The size of the progress bar indicating the charge of the item (in pixels).")] public Vector2 IndicatorSize { get { return indicatorSize; } set { indicatorSize = value; } } - [Serialize(false, true, description: "Should the progress bar indicating the charge of the item fill up horizontally or vertically.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge of the item fill up horizontally or vertically.")] public bool IsHorizontal { get { return isHorizontal; } set { isHorizontal = value; } } - [Editable, Serialize(10.0f, true, description: "Maximum output of the device when fully charged (kW).")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "Maximum output of the device when fully charged (kW).")] public float MaxOutPut { set; get; } - [Editable, Serialize(10.0f, true, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] public float Capacity { get { return capacity; } set { capacity = Math.Max(value, 1.0f); } } - [Editable, Serialize(0.0f, true, description: "The current charge of the device.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current charge of the device.")] public float Charge { get { return charge; } @@ -87,14 +92,14 @@ namespace Barotrauma.Items.Components public float ChargePercentage => MathUtils.Percentage(Charge, Capacity); - [Editable, Serialize(10.0f, true, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] public float MaxRechargeSpeed { get { return maxRechargeSpeed; } set { maxRechargeSpeed = Math.Max(value, 1.0f); } } - [Editable, Serialize(10.0f, true, description: "The current recharge speed of the device.")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The current recharge speed of the device.")] public float RechargeSpeed { get { return rechargeSpeed; } @@ -110,14 +115,14 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, true, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] public bool ExponentialRechargeSpeed { get; set; } - [Editable(minValue: 0.0f, maxValue: 10.0f, decimals: 2), Serialize(0.5f, true)] + [Editable(minValue: 0.0f, maxValue: 10.0f, decimals: 2), Serialize(0.5f, IsPropertySaveable.Yes)] public float RechargeAdjustSpeed { get; set; } private float efficiency; - [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, true, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] + [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, IsPropertySaveable.Yes, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] public float Efficiency { get { return efficiency; } @@ -130,7 +135,7 @@ namespace Barotrauma.Items.Components private bool isRunning; public bool HasBeenTuned { get; private set; } - public PowerContainer(Item item, XElement element) + public PowerContainer(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -154,92 +159,142 @@ namespace Barotrauma.Items.Components isRunning = true; float chargeRatio = charge / capacity; - float gridPower = 0.0f; - float gridLoad = 0.0f; - foreach (Connection c in item.Connections) - { - if (!c.IsPower || !c.IsOutput) { continue; } - foreach (Connection c2 in c.Recipients) - { - if (c2.Item.Condition <= 0.0f) { continue; } - - PowerTransfer pt = c2.Item.GetComponent(); - if (pt == null) - { - foreach (Powered powered in c2.Item.GetComponents()) - { - if (!powered.IsActive) continue; - gridLoad += powered.CurrPowerConsumption; - } - continue; - } - if (!pt.IsActive || !pt.CanTransfer) { continue; } - gridPower -= pt.CurrPowerConsumption; - gridLoad += pt.PowerLoad; - } - } if (chargeRatio > 0.0f) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } - if (charge >= capacity) + float loadReading = 0; + if (powerOut != null && powerOut.Grid != null) { - //rechargeVoltage = 0.0f; - charge = capacity; - CurrPowerConsumption = 0.0f; - } - else - { - float missingCharge = capacity - charge; - float targetRechargeSpeed = rechargeSpeed; - if (ExponentialRechargeSpeed) - { - targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed; - } - if (missingCharge < 1.0f) - { - targetRechargeSpeed *= missingCharge; - } - if (currPowerConsumption < targetRechargeSpeed) - { - currPowerConsumption = Math.Min(currPowerConsumption + deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); - } - else - { - currPowerConsumption = Math.Max(currPowerConsumption - deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); - } - Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f * efficiency; - } - - if (charge <= 0.0f) - { - CurrPowerOutput = 0.0f; - charge = 0.0f; - return; - } - else - { - //output starts dropping when the charge is less than 10% - float maxOutputRatio = 1.0f; - if (chargeRatio < 0.1f) - { - maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f); - } - - CurrPowerOutput += (gridLoad - gridPower) * deltaTime; - - float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad); - CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput); - Charge -= CurrPowerOutput / 3600.0f; + loadReading = powerOut.Grid.Load; } + item.SendSignal(((int)Math.Round(-CurrPowerOutput)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out"); item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%"); item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate"); } + /// + /// Returns the power consumption if checking the powerIn connection, or a negative value if the output can provide power when checking powerOut. + /// Power consumption is proportional to set recharge speed and if there is less than max charge. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection == powerIn) + { + //Don't draw power if fully charged + if (charge >= capacity) + { + charge = capacity; + return 0; + } + else + { + float missingCharge = capacity - charge; + float targetRechargeSpeed = rechargeSpeed; + + if (ExponentialRechargeSpeed) + { + targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed; + } + //For the last kwMin scale the recharge rate linearly to prevent overcharging and to have a smooth cutoff + if (missingCharge < 1.0f) + { + targetRechargeSpeed *= missingCharge; + } + + return MathHelper.Clamp(targetRechargeSpeed, 0, MaxRechargeSpeed); + } + } + else + { + CurrPowerOutput = 0; + return charge > 0 ? -1 : 0; + } + } + + /// + /// Minimum and maximum output for the queried connection. + /// Powerin min max equals CurrPowerConsumption as its abnormal for there to be power out. + /// PowerOut min power out is zero and max is the maxout unless below 10% charge where + /// the output is scaled relative to the 10% charge. + /// + /// Connection being queried + /// Current grid load + /// Minimum and maximum power output for the connection + public override PowerRange MinMaxPowerOut(Connection connection, float load = 0) + { + if (connection == powerOut) + { + float maxOutput; + float chargeRatio = charge / capacity; + if (chargeRatio < 0.1f) + { + maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut; + } + else + { + maxOutput = MaxOutPut; + } + + //Limit max power out to not exceed the charge of the container + maxOutput = Math.Min(maxOutput, charge * 60 / UpdateInterval); + return new PowerRange(0.0f, maxOutput); + } + + return PowerRange.Zero; + } + + /// + /// Finalized power out from the container for the connection, provided the given grid information + /// Output power based on the maxpower all batteries can output. So all batteries can + /// equally share powerout based on their output capabilities. + /// + /// + /// + /// + /// + /// + public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) + { + if (connection == powerOut) + { + //Calculate the max power the container can output + float maxPowerOutput = MaxOutPut; + float chargeRatio = charge / capacity; + if (chargeRatio < 0.1f) + { + maxPowerOutput *= Math.Max(chargeRatio * 10.0f, 0.0f); + } + + //Set power output based on the relative max power output capabilities and load demand + CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * maxPowerOutput; + return CurrPowerOutput; + } + return 0.0f; + } + + /// + /// When the corresponding grid connection is resolved, adjust the container's charge. + /// + public override void GridResolved(Connection conn) + { + if (conn == powerIn) + { + //Increase charge based on how much power came in from the grid + Charge += (CurrPowerConsumption * Voltage) / 60 * UpdateInterval * efficiency; + } + else + { + //Decrease charge based on how much power is leaving the device + Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity); + } + } + public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } @@ -250,8 +305,8 @@ namespace Barotrauma.Items.Components } if (HasBeenTuned) { return true; } - float targetRatio = string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase) ? aiRechargeTargetRatio : -1; - if (targetRatio > 0 || float.TryParse(objective.Option, out targetRatio)) + float targetRatio = objective.Option.IsEmpty || objective.Option == "charge" ? aiRechargeTargetRatio : -1; + if (targetRatio > 0 || float.TryParse(objective.Option.Value, out targetRatio)) { if (Math.Abs(rechargeSpeed - maxRechargeSpeed * targetRatio) > 0.05f) { @@ -267,9 +322,10 @@ namespace Barotrauma.Items.Components #endif if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", + ("[itemname]", item.Name, FormatCapitals.Yes), + ("[rate]", ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString(), FormatCapitals.No)).Value, + null, 1.0f, "chargebattery".ToIdentifier(), 10.0f); } } } @@ -289,9 +345,10 @@ namespace Barotrauma.Items.Components #endif if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", + ("[itemname]", item.Name, FormatCapitals.Yes), + ("[rate]", ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString(), FormatCapitals.No)).Value, + null, 1.0f, "chargebattery".ToIdentifier(), 10.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 33f934b24..82db50a9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -26,18 +26,25 @@ namespace Barotrauma.Items.Components public float PowerLoad { - get { return powerLoad; } + get + { + if (this is RelayComponent || PowerConnections.Count == 0 || PowerConnections[0].Grid == null) + { + return powerLoad; + } + return PowerConnections[0].Grid.Load; + } set { powerLoad = value; } } - [Editable, Serialize(true, true, description: "Can the item be damaged if too much power is supplied to the power grid.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged if too much power is supplied to the power grid.")] public bool CanBeOverloaded { get; set; } - [Editable(MinValueFloat = 1.0f), Serialize(2.0f, true, description: + [Editable(MinValueFloat = 1.0f), Serialize(2.0f, IsPropertySaveable.Yes, description: "How much power has to be supplied to the grid relative to the load before item starts taking damage. " + "E.g. a value of 2 means that the grid has to be receiving twice as much power as the devices in the grid are consuming.")] public float OverloadVoltage @@ -46,14 +53,14 @@ namespace Barotrauma.Items.Components set; } - [Serialize(0.15f, true, description: "The probability for a fire to start when the item breaks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.15f, IsPropertySaveable.Yes, description: "The probability for a fire to start when the item breaks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float FireProbability { get; set; } - [Serialize(false, false, description: "Is the item currently overloaded. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(false, IsPropertySaveable.No, description: "Is the item currently overloaded. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public bool Overload { get; @@ -62,6 +69,7 @@ namespace Barotrauma.Items.Components private float extraLoad; private float extraLoadSetTime; + /// /// Additional load coming from somewhere else than the devices connected to the junction box (e.g. ballast flora or piezo crystals). /// Goes back to zero automatically if you stop setting the value. @@ -71,7 +79,7 @@ namespace Barotrauma.Items.Components get { return extraLoad; } set { - extraLoad = Math.Max(value, 0.0f); + extraLoad = value; extraLoadSetTime = (float)Timing.TotalTime; } } @@ -112,7 +120,7 @@ namespace Barotrauma.Items.Components } } - public PowerTransfer(Item item, XElement element) + public PowerTransfer(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -168,9 +176,34 @@ namespace Barotrauma.Items.Components { RefreshConnections(); + float powerReadingOut = 0; + float loadReadingOut = ExtraLoad; + if (powerLoad < 0) + { + powerReadingOut = -powerLoad; + loadReadingOut = 0; + } + + if (powerOut != null && powerOut.Grid != null) + { + powerReadingOut = powerOut.Grid.Power; + loadReadingOut = powerOut.Grid.Load; + } + + item.SendSignal(((int)Math.Round(powerReadingOut)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(loadReadingOut)).ToString(), "load_value_out"); + if (Timing.TotalTime > extraLoadSetTime + 1.0) { - extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0); + //Decay the extra load to 0 from either positive or negative + if (extraLoad > 0) + { + extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0); + } + else + { + extraLoad = Math.Min(extraLoad + 1000.0f * deltaTime, 0); + } } if (!CanTransfer) { return; } @@ -200,7 +233,9 @@ namespace Barotrauma.Items.Components item.SendSignal(loadSignal, "load_value_out"); float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); - Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage; + + Overload = Voltage > maxOverVoltage; + if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { if (overloadCooldownTimer > 0.0f) @@ -239,6 +274,11 @@ namespace Barotrauma.Items.Components } } + public override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load) + { + return conn == powerOut ? PowerConsumption + ExtraLoad : 0; + } + public override bool Pick(Character picker) { return picker != null; @@ -376,25 +416,6 @@ namespace Barotrauma.Items.Components SetAllConnectionsDirty(); } - public override void ReceivePowerProbeSignal(Connection connection, Item source, float power) - { - //we've already received this signal - if (lastPowerProbeRecipients.Contains(this)) { return; } - if (item.Condition <= 0.0f) { return; } - - lastPowerProbeRecipients.Add(this); - - if (power < 0.0f) - { - powerLoad -= power; - } - else - { - currPowerConsumption -= power; - } - powerOut?.SendPowerProbeSignal(source, power); - } - public override void ReceiveSignal(Signal signal, Connection connection) { if (item.Condition <= 0.0f || connection.IsPower) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 07e8ab3f3..7d0e21ff8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -8,10 +8,57 @@ using Barotrauma.Sounds; namespace Barotrauma.Items.Components { + /// + /// Order in which power sources will provide to a grid, lower number is higher priority + /// + public enum PowerPriority + { + Default = 0, // Use for status effects and/or extraload + Reactor = 1, + Relay = 3, + Battery = 5 + } + + readonly struct PowerRange + { + public readonly static PowerRange Zero = default; + + public readonly float Min; + public readonly float Max; + + /// + /// Used by reactors to communicate their maximum output to each other so they can divide the grid load between each other in a sensible way + /// + public readonly float ReactorMaxOutput; + + public PowerRange(float min, float max) : this(min, max, 0.0f) + { + } + + public PowerRange(float min, float max, float reactorMaxOutput) + { + System.Diagnostics.Debug.Assert(max >= min); + System.Diagnostics.Debug.Assert(min >= 0); + System.Diagnostics.Debug.Assert(max >= 0); + Min = min; + Max = max; + ReactorMaxOutput = reactorMaxOutput; + } + + public static PowerRange operator +(PowerRange a, PowerRange b) + { + return new PowerRange(a.Min + b.Min, a.Max + b.Max, a.ReactorMaxOutput + b.ReactorMaxOutput); + } + public static PowerRange operator -(PowerRange a, PowerRange b) + { + return new PowerRange(a.Min - b.Min, a.Max - b.Max, a.ReactorMaxOutput - b.ReactorMaxOutput); + } + } + partial class Powered : ItemComponent { - private static float updateTimer; - protected static float UpdateInterval = 0.2f; + //TODO: test sparser update intervals? + protected const float UpdateInterval = (float)Timing.Step; /// /// List of all powered ItemComponents @@ -22,10 +69,9 @@ namespace Barotrauma.Items.Components get { return poweredList; } } - /// - /// Items that have already received the "probe signal" that's used to distribute power and load across the grid - /// - protected static HashSet lastPowerProbeRecipients = new HashSet(); + public static readonly List ChangedConnections = new List(); + + public readonly static Dictionary Grids = new Dictionary(); /// /// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items @@ -49,7 +95,9 @@ namespace Barotrauma.Items.Components protected Connection powerIn, powerOut; - [Editable, Serialize(0.5f, true, description: "The minimum voltage required for the device to function. " + + protected virtual PowerPriority Priority { get { return PowerPriority.Default; } } + + [Editable, Serialize(0.5f, IsPropertySaveable.Yes, description: "The minimum voltage required for the device to function. " + "The voltage is calculated as power / powerconsumption, meaning that a device " + "with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")] public float MinVoltage @@ -58,14 +106,14 @@ namespace Barotrauma.Items.Components set { minVoltage = value; } } - [Editable, Serialize(0.0f, true, description: "How much power the device draws (or attempts to draw) from the electrical grid when active.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How much power the device draws (or attempts to draw) from the electrical grid when active.")] public float PowerConsumption { get { return powerConsumption; } set { powerConsumption = value; } } - [Serialize(false, true, description: "Is the device currently active. Inactive devices don't consume power.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the device currently active. Inactive devices don't consume power.")] public override bool IsActive { get { return base.IsActive; } @@ -79,35 +127,63 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true, description: "The current power consumption of the device. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The current power consumption of the device. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float CurrPowerConsumption { get {return currPowerConsumption; } set { currPowerConsumption = value; } } - [Serialize(0.0f, true, description: "The current voltage of the item (calculated as power consumption / available power). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The current voltage of the item (calculated as power consumption / available power). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float Voltage { - get { return voltage; } - set { voltage = Math.Max(0.0f, value); } + get + { + if (powerIn != null) + { + if (powerIn?.Grid != null) { return powerIn.Grid.Voltage; } + } + else if (powerOut != null) + { + if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; } + } + return voltage; + } + set + { + if (powerIn != null) + { + if (powerIn.Grid != null) + { + powerIn.Grid.Voltage = Math.Max(0.0f, value); + } + } + else if (powerOut != null) + { + if (powerOut.Grid != null) + { + powerOut.Grid.Voltage = Math.Max(0.0f, value); + } + } + voltage = Math.Max(0.0f, value); + } } - [Editable, Serialize(true, true, description: "Can the item be damaged by electomagnetic pulses.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged by electomagnetic pulses.")] public bool VulnerableToEMP { get; set; } - public Powered(Item item, XElement element) + public Powered(Item item, ContentXElement element) : base(item, element) { poweredList.Add(this); InitProjectSpecific(element); } - partial void InitProjectSpecific(XElement element); + partial void InitProjectSpecific(ContentXElement element); protected void UpdateOnActiveEffects(float deltaTime) { @@ -115,19 +191,19 @@ namespace Barotrauma.Items.Components { //if the item consumes no power, ignore the voltage requirement and //apply OnActive statuseffects as long as this component is active - if (powerConsumption <= 0.0f) + if (PowerConsumption <= 0.0f) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } return; } - if (voltage > minVoltage) + if (Voltage > minVoltage) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } #if CLIENT - if (voltage > minVoltage) + if (Voltage > minVoltage) { if (!powerOnSoundPlayed && powerOnSound != null) { @@ -135,7 +211,7 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = true; } } - else if (voltage < 0.1f) + else if (Voltage < 0.1f) { powerOnSoundPlayed = false; } @@ -144,7 +220,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - currPowerConsumption = powerConsumption; UpdateOnActiveEffects(deltaTime); } @@ -163,6 +238,7 @@ namespace Barotrauma.Items.Components else if (c.Name == "power_out") { powerOut = c; + powerOut.Priority = Priority; } else if (c.Name == "power") { @@ -182,6 +258,7 @@ namespace Barotrauma.Items.Components #endif } powerOut = c; + powerOut.Priority = Priority; } else { @@ -199,104 +276,388 @@ namespace Barotrauma.Items.Components } } - public virtual void ReceivePowerProbeSignal(Connection connection, Item source, float power) { } + /// + /// Allocate electrical devices into their grids based on connections + /// + /// Use previous grids and change in connections + public static void UpdateGrids(bool useCache = true) + { + //don't use cache if there are no existing grids + if (Grids.Count > 0 && useCache) + { + //delete all grids that were affected + foreach (Connection c in ChangedConnections) + { + if (c.Grid != null) + { + Grids.Remove(c.Grid.ID); + c.Grid = null; + } + } + + foreach (Connection c in ChangedConnections) + { + //Make sure the connection grid hasn't been resolved by another connection update + //Ensure the connection has other connections + if (c.Grid == null && c.Recipients.Count > 0 && c.Item.Condition > 0.0f) + { + GridInfo grid = PropagateGrid(c); + Grids[grid.ID] = grid; + } + } + } + else + { + //Clear all grid IDs from connections + foreach (Powered powered in poweredList) + { + //Only check devices with connectors + if (powered.powerIn != null) + { + powered.powerIn.Grid = null; + } + if (powered.powerOut != null) + { + powered.powerOut.Grid = null; + } + } + + Grids.Clear(); + + foreach (Powered powered in poweredList) + { + //Probe through all connections that don't have a gridID + if (powered.powerIn != null && powered.powerIn.Grid == null && powered.powerIn != powered.powerOut && powered.Item.Condition > 0.0f) + { + // Only create grids for networks with more than 1 device + if (powered.powerIn.Recipients.Count > 0) + { + GridInfo grid = PropagateGrid(powered.powerIn); + Grids[grid.ID] = grid; + } + } + + if (powered.powerOut != null && powered.powerOut.Grid == null && powered.Item.Condition > 0.0f) + { + //Only create grids for networks with more than 1 device + if (powered.powerOut.Recipients.Count > 0) + { + GridInfo grid = PropagateGrid(powered.powerOut); + Grids[grid.ID] = grid; + } + } + } + } + + //Clear changed connections after each update + ChangedConnections.Clear(); + } + + private static GridInfo PropagateGrid(Connection conn) + { + //Generate unique Key + int id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced); + while (Grids.ContainsKey(id)) + { + id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced); + } + + return PropagateGrid(conn, id); + } + + private static GridInfo PropagateGrid(Connection conn, int gridID) + { + Stack probeStack = new Stack(); + + GridInfo grid = new GridInfo(gridID); + + probeStack.Push(conn); + + //Non recursive approach to traversing connection tree + while (probeStack.Count > 0) + { + Connection c = probeStack.Pop(); + c.Grid = grid; + grid.AddConnection(c); + + //Add on recipients + foreach (Connection otherC in c.Recipients) + { + //Only add valid connections + if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC)) + { + if (otherC.Item.Condition <= 0.0f) + { + continue; + } + + otherC.Grid = grid; //Assigning ID early prevents unncessary adding to stack + probeStack.Push(otherC); + } + } + } + + return grid; + } + + /// + /// Update the power calculations of all devices and grids + /// Updates grids in the order of + /// ConnCurrConsumption - Get load of device/ flag it as an outputting connection + /// -- If outputting power -- + /// MinMaxPower - Minimum and Maximum power output of the connection for devices to coordinate + /// ConnPowerOut - Final power output based on the sum of the MinMaxPower + /// -- Finally -- + /// GridResolved - Indicate that a connection's grid has been finished being calculated + /// + /// Power outputting devices are calculated in stages based on their priority + /// Reactors will output first, followed by relays then batteries. + /// + /// + /// public static void UpdatePower(float deltaTime) { + //Don't update the power if the round is ending + if (GameMain.GameSession != null && GameMain.GameSession.RoundEnding) + { + return; + } + + //Only update the power at the given update interval + /* + //Not use currently as update interval of 1/60 if (updateTimer > 0.0f) { updateTimer -= deltaTime; return; } updateTimer = UpdateInterval; + */ - //reset power first - foreach (Powered powered in poweredList) +#if CLIENT + System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); + sw.Start(); +#endif + //Ensure all grids are updated correctly and have the correct connections + UpdateGrids(); + +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("GridUpdate", sw.ElapsedTicks); + sw.Restart(); +#endif + + //Reset all grids + foreach (GridInfo grid in Grids.Values) { - if (powered is PowerTransfer pt) - { - powered.CurrPowerConsumption = 0.0f; - pt.PowerLoad = 0.0f; - if (pt is RelayComponent relay) - { - relay.DisplayLoad = 0.0f; - } - } - //only reset voltage if the item has a power connector - //(other items, such as handheld devices, get power through other means and shouldn't be updated here) - if (powered.powerIn != null || powered.powerOut != null) { powered.voltage = 0.0f; } + //Wipe priority groups as connections can change to not be outputting -- Can be improved caching wise -- + grid.PowerSourceGroups.Clear(); + grid.Power = 0; + grid.Load = 0; } - //go through all the devices that are consuming/providing power - //and send out a "probe signal" which the PowerTransfer components use to add up the grid power/load + //Determine if devices are adding a load or providing power, also resolve solo nodes foreach (Powered powered in poweredList) { - if (powered is PowerTransfer pt) + //Handle the device if it's got a power connection + if (powered.powerIn != null && powered.powerOut != powered.powerIn) { - if (pt.ExtraLoad > 0.0f) - { - lastPowerProbeRecipients.Clear(); - powered.powerIn?.SendPowerProbeSignal(powered.item, -pt.ExtraLoad); + //Get the new load for the connection + float currLoad; + if (powered.Item.GetComponent() is Repairable repairable && repairable.IsTinkering && repairable.TinkeringPowersDevices && !(powered is PowerContainer)) + { + currLoad = 0.0f; + } + else + { + currLoad = powered.GetCurrentPowerConsumption(powered.powerIn); } - continue; - } - else if (powered.currPowerConsumption > 0.0f) - { - //consuming power - lastPowerProbeRecipients.Clear(); - powered.powerIn?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption); - } - } - foreach (Powered powered in poweredList) - { - if (powered is PowerTransfer) { continue; } - else if (powered.currPowerConsumption < 0.0f) - { - //providing power - lastPowerProbeRecipients.Clear(); - powered.powerOut?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption); - } - if (powered is PowerContainer pc) - { - if (pc.CurrPowerOutput <= 0.0f || pc.item.Condition <= 0.0f) { continue; } - //providing power - lastPowerProbeRecipients.Clear(); - powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput); - } - } - //go through powered items and calculate their current voltage - foreach (Powered powered in poweredList) - { - if (powered is PowerTransfer pt1 || (pt1 = powered.Item.GetComponent()) != null) - { - powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f); - continue; - } - if ((powered.powerConsumption <= 0.0f || (powered.Item.GetComponent() is Repairable repairable && repairable.IsTinkering && repairable.TinkeringPowersDevices)) && !(powered is PowerContainer)) - { - powered.voltage = 1.0f; - continue; - } - if (powered.powerIn == null) { continue; } - foreach (Connection powerSource in powered.powerIn.Recipients) - { - if (!powerSource.IsPower || !powerSource.IsOutput) { continue; } - var pt = powerSource.Item.GetComponent(); - if (pt != null) + //If its a load update its grid load + if (currLoad >= 0) { - float voltage = -pt.CurrPowerConsumption / Math.Max(pt.PowerLoad, 1.0f); - powered.voltage = Math.Max(powered.voltage, voltage); - continue; + powered.CurrPowerConsumption = currLoad; + if (powered.powerIn.Grid != null) + { + powered.powerIn.Grid.Load += currLoad; + } } - var pc = powerSource.Item.GetComponent(); - if (pc != null && pc.item.Condition > 0.0f) + else if (powered.powerIn.Grid != null) { - float voltage = pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f); - powered.voltage += voltage; + //If connected to a grid add as a source to be processed + powered.powerIn.Grid.AddSrc(powered.powerIn); + } + else + { + powered.CurrPowerConsumption = powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); + powered.GridResolved(powered.powerIn); + } + } + + //Handle the device power depending on if its powerout + if (powered.powerOut != null) + { + //Get the connection's load + float currLoad = powered.GetCurrentPowerConsumption(powered.powerOut); + + //Update the device's output load to the correct variable + if (powered is PowerTransfer pt) + { + pt.PowerLoad = currLoad; + } + else if (powered is PowerContainer pc) + { + // PowerContainer handle its own output value + } + else + { + powered.CurrPowerConsumption = currLoad; + } + + if (currLoad >= 0) + { + //Add to the grid load if possible + if (powered.powerOut.Grid != null) + { + powered.powerOut.Grid.Load += currLoad; + } + } + else if (powered.powerOut.Grid != null) + { + //Add connection as a source to be processed + powered.powerOut.Grid.AddSrc(powered.powerOut); + } + else + { + //Perform power calculations for the singular connection + float loadOut = powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); + if (powered is PowerTransfer pt2) + { + pt2.PowerLoad = loadOut; + } + else if (powered is PowerContainer pc) + { + //PowerContainer handles its own output value + } + else + { + powered.CurrPowerConsumption = loadOut; + } + + //Indicate grid is resolved as it was the only device + powered.GridResolved(powered.powerOut); } } } + + //Iterate through all grids to determine the power on the grid + foreach (GridInfo grid in Grids.Values) + { + //Iterate through the priority src groups lowest first + foreach (PowerSourceGroup scrGroup in grid.PowerSourceGroups.Values) + { + scrGroup.MinMaxPower = PowerRange.Zero; + + //Iterate through all connections in the group to get their minmax power and sum them + foreach (Connection c in scrGroup.Connections) + { + Powered device = c.Item.GetComponent(); + scrGroup.MinMaxPower += device.MinMaxPowerOut(c, grid.Load); + } + + //Iterate through all connections to get their final power out provided the min max information + float addedPower = 0; + foreach (Connection c in scrGroup.Connections) + { + Powered device = c.Item.GetComponent(); + addedPower += device.GetConnectionPowerOut(c, grid.Power, scrGroup.MinMaxPower, grid.Load); + } + + //Add the power to the grid + grid.Power += addedPower; + } + + //Calculate Grid voltage, limit between 0 - 1000 + float newVoltage = MathHelper.Min(grid.Power / MathHelper.Max(grid.Load, 1E-10f), 1000); + if (float.IsNegative(newVoltage)) + { + newVoltage = 0.0f; + } + + grid.Voltage = newVoltage; + + //Iterate through all connections on that grid and run their gridResolved function + foreach (Connection con in grid.Connections) + { + Powered device = con.Item.GetComponent(); + device.GridResolved(con); + } + } + +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("PowerUpdate", sw.ElapsedTicks); +#endif + } + + /// + /// Current power consumption of the device (or amount of generated power if negative) + /// + /// Connection to calculate power consumption for. + public virtual float GetCurrentPowerConsumption(Connection connection = null) + { + // If a handheld device there is no consumption + if (powerIn == null && powerOut == null) + { + return 0; + } + + // Add extraload for PowerTransfer devices + if (this is PowerTransfer pt) + { + return PowerConsumption + pt.ExtraLoad; + } + else if (connection != this.powerIn || !IsActive) + { + //If not the power in connection or is inactive there is no draw + return 0; + } + + //Otherwise return the max powerconsumption of the device + return PowerConsumption; + } + + /// + /// Minimum and maximum power the connection can provide + /// + /// Connection being queried about its power capabilities + /// Load of the connected grid + public virtual PowerRange MinMaxPowerOut(Connection conn, float load = 0) + { + return PowerRange.Zero; + } + + /// + /// Finalize how much power the device will be outputting to the connection + /// + /// Connection being queried + /// Current grid power + /// Current load on the grid + /// Power pushed to the grid + public virtual float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load) + { + return conn == powerOut ? MathHelper.Max(-CurrPowerConsumption, 0) : 0; + } + + /// + /// Can be overridden to perform updates for the device after the connected grid has resolved its power calculations, i.e. storing voltage for later updates + /// + public virtual void GridResolved(Connection conn) { } + + public static bool ValidPowerConnection(Connection conn1, Connection conn2) + { + return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.IsOutput != conn2.IsOutput || (conn1.Item.HasTag("dock") && conn2.Item.HasTag("dock"))); } /// @@ -314,7 +675,6 @@ namespace Barotrauma.Items.Components if (!recipient.IsPower || !recipient.IsOutput) { continue; } var battery = recipient.Item?.GetComponent(); if (battery == null) { continue; } - float maxOutputPerFrame = battery.MaxOutPut / 60.0f; float framesPerMinute = 3600.0f; availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame); @@ -323,10 +683,119 @@ namespace Barotrauma.Items.Components return availablePower; } + /// + /// Efficient method to retrieve the batteries connected to the device + /// + /// All connected PowerContainers + protected List GetConnectedBatteries(bool outputOnly = true) + { + List batteries = new List(); + GridInfo supplyingGrid = null; + + //Determine supplying grid, prefer PowerIn connection + if (powerIn != null) + { + if (powerIn.Grid != null) + { + supplyingGrid = powerIn.Grid; + } + } + else if (powerOut != null) + { + if (powerOut.Grid != null) + { + supplyingGrid = powerOut.Grid; + } + } + + if (supplyingGrid != null) + { + //Iterate through all connections to fine powerContainers + foreach (Connection c in supplyingGrid.Connections) + { + PowerContainer pc = c.Item.GetComponent(); + if (pc != null && (!outputOnly || pc.powerOut == c)) + { + batteries.Add(pc); + } + } + } + + return batteries; + } + protected override void RemoveComponentSpecific() { + //Flag power connections to be updated + if (item.Connections != null) + { + foreach (Connection c in item.Connections) + { + if (c.IsPower && c.Grid != null) + { + ChangedConnections.Add(c); + } + } + } + base.RemoveComponentSpecific(); poweredList.Remove(this); } } + + partial class GridInfo + { + public readonly int ID; + public float Voltage = 0; + public float Load = 0; + public float Power = 0; + + public readonly List Connections = new List(); + public readonly SortedList PowerSourceGroups = new SortedList(); + + public GridInfo(int id) + { + ID = id; + } + + public void RemoveConnection(Connection c) + { + Connections.Remove(c); + + //Remove the grid if it has no devices + if (Connections.Count == 0 && Powered.Grids.ContainsKey(ID)) + { + Powered.Grids.Remove(ID); + } + } + + public void AddConnection(Connection c) + { + Connections.Add(c); + } + + public void AddSrc(Connection c) + { + if (PowerSourceGroups.ContainsKey(c.Priority)) + { + PowerSourceGroups[c.Priority].Connections.Add(c); + } + else + { + PowerSourceGroup group = new PowerSourceGroup(); + group.Connections.Add(c); + PowerSourceGroups[c.Priority] = group; + } + } + } + + partial class PowerSourceGroup + { + public PowerRange MinMaxPower; + public readonly List Connections = new List(); + + public PowerSourceGroup() + { + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fb4d7bbd1..2890cc214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -88,13 +88,13 @@ namespace Barotrauma.Items.Components private float persistentStickJointTimer; - [Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] + [Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] public float LaunchImpulse { get; set; } - [Serialize(0.0f, false, description: "The random percentage modifier used to add variance to the launch impulse.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The random percentage modifier used to add variance to the launch impulse.")] public float ImpulseSpread { get; set; } - [Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] public float LaunchRotation { @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components private set; } - [Serialize(false, false, description: "When set to true, the item can stick to any target it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item can stick to any target it hits.")] //backwards compatibility, can stick to anything public bool DoesStick { @@ -116,50 +116,50 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] + [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] public bool StickPermanently { get; set; } - [Serialize(false, false, description: "Can the item stick to the character it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { get; set; } - [Serialize(false, false, description: "Can the item stick to the structure it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the structure it hits.")] public bool StickToStructures { get; set; } - [Serialize(false, false, description: "Can the item stick to the item it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the item it hits.")] public bool StickToItems { get; set; } - [Serialize(false, false, description: "Can the item stick even to deflective targets.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")] public bool StickToDeflective { get; set; } - [Serialize(false, false, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ - "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] + [Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ + "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan { get; set; } - [Serialize(1, false, description: "How many hitscans should be done when the projectile is launched. " + [Serialize(1, IsPropertySaveable.No, description: "How many hitscans should be done when the projectile is launched. " + "Multiple hitscans can be used to simulate weapons that fire multiple projectiles at the same time" + " without having to actually use multiple projectile items, for example shotguns.")] public int HitScanCount @@ -168,28 +168,28 @@ namespace Barotrauma.Items.Components set; } - [Serialize(1, false, description: "How many targets the projectile can hit before it stops.")] + [Serialize(1, IsPropertySaveable.No, description: "How many targets the projectile can hit before it stops.")] public int MaxTargetsToHit { get; set; } - [Serialize(false, false, description: "Should the item be deleted when it hits something.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it hits something.")] public bool RemoveOnHit { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the launch angle of the projectile (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the launch angle of the projectile (in degrees).")] public float Spread { get; set; } - [Serialize(false, false, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] + [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] public bool StaticSpread { get; @@ -198,7 +198,7 @@ namespace Barotrauma.Items.Components private float deactivationTimer; - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float DeactivationTime { get; @@ -219,19 +219,19 @@ namespace Barotrauma.Items.Components private Category originalCollisionCategories; private Category originalCollisionTargets; - public Projectile(Item item, XElement element) + public Projectile(Item item, ContentXElement element) : base (item, element) { IgnoredBodies = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } Attack = new Attack(subElement, item.Name + ", Projectile", item); } InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void OnItemLoaded() { @@ -484,7 +484,7 @@ namespace Barotrauma.Items.Components } else { - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } } } @@ -697,27 +697,9 @@ namespace Barotrauma.Items.Components return false; } if (hits.Contains(target.Body)) { return false; } - if (target.Body.UserData is Submarine sub) + if (ShouldIgnoreSubmarineCollision(target, contact)) { - Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? - contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); - - //do a raycast in the sub's coordinate space to see if it hit a structure - var wallBody = Submarine.PickBody( - item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, - item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, - collisionCategory: Physics.CollisionWall); - if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && - //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) - { - target = wallBody.FixtureList.First(); - if (hits.Contains(target.Body)) { return false; } - } - else - { - return false; - } + return false; } else if (target.Body.UserData is Limb limb) { @@ -757,6 +739,44 @@ namespace Barotrauma.Items.Components } } + /// + /// Should the collision with the target submarine be ignored (e.g. did the projectile collide with the wall behind the turret when being launched) + /// + /// Fixture the projectile hit + /// Contact between the projectile and the target + /// True if the target isn't a submarine or if the collision happened behind the launch position of the projectile + public bool ShouldIgnoreSubmarineCollision(Fixture target, Contact contact) + { + return ShouldIgnoreSubmarineCollision(ref target, contact); + } + + private bool ShouldIgnoreSubmarineCollision(ref Fixture target, Contact contact) + { + if (target.Body.UserData is Submarine sub) + { + Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? + contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); + + //do a raycast in the sub's coordinate space to see if it hit a structure + var wallBody = Submarine.PickBody( + item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, + item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, + collisionCategory: Physics.CollisionWall); + if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && + //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction + Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) + { + target = wallBody.FixtureList.First(); + if (hits.Contains(target.Body)) { return true; } + } + else + { + return true; + } + } + return false; + } + private readonly List targets = new List(); private Fixture lastTarget; @@ -961,7 +981,7 @@ namespace Barotrauma.Items.Components removePending = true; item.HiddenInGame = true; item.body.FarseerBody.Enabled = false; - Entity.Spawner?.AddToRemoveQueue(item); + Entity.Spawner?.AddItemToRemoveQueue(item); } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 090028dc7..0be5f9561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components private int qualityLevel; - [Editable, Serialize(0, true)] + [Editable, Serialize(0, IsPropertySaveable.Yes)] public int QualityLevel { get { return qualityLevel; } @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components } } - public Quality(Item item, XElement element) : base(item, element) + public Quality(Item item, ContentXElement element) : base(item, element) { foreach (XElement subElement in element.Elements()) { @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components string statTypeString = subElement.GetAttributeString("stattype", ""); if (!Enum.TryParse(statTypeString, true, out StatType statType)) { - DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + item.prefab.Identifier + ")"); + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")"); } float statValue = subElement.GetAttributeFloat("value", 0f); statValues.TryAdd(statType, statValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs index 3643d43b1..d457cb589 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs @@ -5,21 +5,21 @@ namespace Barotrauma.Items.Components { partial class RemoteController : ItemComponent { - [Serialize("", false, description: "Tag or identifier of the item that should be controlled.")] + [Serialize("", IsPropertySaveable.No, description: "Tag or identifier of the item that should be controlled.")] public string Target { get; private set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool OnlyInOwnSub { get; private set; } - [Serialize(10000.0f, false)] + [Serialize(10000.0f, IsPropertySaveable.No)] public float Range { get; @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components private Character currentUser; private Submarine currentSub; - public RemoteController(Item item, XElement element) + public RemoteController(Item item, ContentXElement element) : base(item, element) { } @@ -81,7 +81,7 @@ namespace Barotrauma.Items.Components if (targetItem.Submarine != item.Submarine) { continue; } if (targetItem.Submarine.TeamID != user.TeamID) { continue; } } - if (!targetItem.HasTag(Target) && targetItem.prefab.Identifier != Target) { continue; } + if (!targetItem.HasTag(Target) && ((MapEntity)targetItem).Prefab.Identifier != Target) { continue; } float distSqr = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); if (distSqr > Range * Range || distSqr > closestDist) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 2893f94f1..3e1439340 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { - private readonly string header; + private readonly LocalizedString header; private float deteriorationTimer; private float deteriorateAlwaysResetTimer; @@ -24,63 +24,63 @@ namespace Barotrauma.Items.Components public float LastActiveTime; - [Serialize(0.0f, true, description: "How fast the condition of the item deteriorates per second."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the condition of the item deteriorates per second."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float DeteriorationSpeed { get; set; } - [Serialize(0.0f, true, description: "Minimum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] public float MinDeteriorationDelay { get; set; } - [Serialize(0.0f, true, description: "Maximum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Maximum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] public float MaxDeteriorationDelay { get; set; } - [Serialize(50.0f, true, description: "The item won't deteriorate spontaneously if the condition is below this value. For example, if set to 10, the condition will spontaneously drop to 10 and then stop dropping (unless the item is damaged further by external factors). Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "The item won't deteriorate spontaneously if the condition is below this value. For example, if set to 10, the condition will spontaneously drop to 10 and then stop dropping (unless the item is damaged further by external factors). Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinDeteriorationCondition { get; set; } - [Serialize(0f, true, description: "How low a traitor must get the item's condition for it to start breaking down.")] + [Serialize(0f, IsPropertySaveable.Yes, description: "How low a traitor must get the item's condition for it to start breaking down.")] public float MinSabotageCondition { get; set; } - [Serialize(80.0f, true, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(80.0f, IsPropertySaveable.Yes, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float RepairThreshold { get; set; } - [Serialize(100.0f, true, description: "The amount of time it takes to fix the item with insufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(100.0f, IsPropertySaveable.Yes, description: "The amount of time it takes to fix the item with insufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float FixDurationLowSkill { get; set; } - [Serialize(10.0f, true, description: "The amount of time it takes to fix the item with sufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "The amount of time it takes to fix the item with sufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float FixDurationHighSkill { get; set; } - [Serialize(false, false, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")] + [Serialize(false, IsPropertySaveable.No, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")] public bool DeteriorateAlways { get; @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components private float skillRequirementMultiplier; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float SkillRequirementMultiplier { get { return skillRequirementMultiplier; } @@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components private set { currentFixerAction = value; } } - public Repairable(Item item, XElement element) + public Repairable(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -145,9 +145,9 @@ namespace Barotrauma.Items.Components this.item = item; header = - TextManager.Get(element.GetAttributeString("header", ""), returnNull: true) ?? - TextManager.Get(item.Prefab.ConfigElement.GetAttributeString("header", ""), returnNull: true) ?? - element.GetAttributeString("name", ""); + TextManager.Get(element.GetAttributeString("header", "")).Fallback( + TextManager.Get(item.Prefab.ConfigElement.GetAttributeString("header", ""))).Fallback( + element.GetAttributeString("name", "")); //backwards compatibility var repairThresholdAttribute = @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); /// /// Check if the character manages to succesfully repair the item @@ -183,7 +183,7 @@ namespace Barotrauma.Items.Components if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; } // unpowered (electrical) items can be repaired without a risk of electrical shock - if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) + if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) { if (item.GetComponent() is Reactor reactor) { @@ -244,6 +244,11 @@ namespace Barotrauma.Items.Components } else { + if (CurrentFixerAction == FixActions.Tinker && action != FixActions.Tinker) + { + CurrentFixer?.CheckTalents(AbilityEffectType.OnStopTinkering); + } + Item bestRepairItem = GetBestRepairItem(character); #if SERVER if (CurrentFixer != character || currentFixerAction != action) @@ -260,8 +265,13 @@ namespace Barotrauma.Items.Components return false; } - GameServer.Log($"{GameServer.CharacterLogName(character)} started {(action == FixActions.Sabotage ? "sabotaging" : "repairing")} {item.Name}", ServerLog.MessageType.ItemInteraction); - item.CreateServerEvent(this); + if ((character != prevLoggedFixer || action != prevLoggedFixAction) && (character.TeamID == CharacterTeamType.Team1 || character.TeamID == CharacterTeamType.Team2)) + { + GameServer.Log($"{GameServer.CharacterLogName(character)} started {(action == FixActions.Sabotage ? "sabotaging" : "repairing")} {item.Name}", ServerLog.MessageType.ItemInteraction); + item.CreateServerEvent(this); + prevLoggedFixer = character; + prevLoggedFixAction = action; + } } #else if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character, bestRepairItem)) { return false; } @@ -442,7 +452,7 @@ namespace Barotrauma.Items.Components float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed); - + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); if (currentFixerAction == FixActions.Repair) @@ -478,6 +488,10 @@ namespace Barotrauma.Items.Components deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); wasBroken = false; StopRepairing(CurrentFixer); +#if SERVER + prevLoggedFixer = null; + prevLoggedFixAction = FixActions.None; +#endif } } else if (currentFixerAction == FixActions.Sabotage) @@ -523,11 +537,11 @@ namespace Barotrauma.Items.Components { if (character == null) { return 1.0f; } // kind of rough to keep this in update, but seems most robust - if (requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical", StringComparison.OrdinalIgnoreCase))) + if (requiredSkills.Any(s => s != null && s.Identifier == "mechanical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical); } - if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) + if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 432e96f31..7c5b320ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -18,56 +18,56 @@ namespace Barotrauma.Items.Components private float raycastTimer; private const float RayCastInterval = 0.2f; - [Serialize(0.0f, false, description: "How much force is applied to pull the projectile the rope is attached to.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the projectile the rope is attached to.")] public float ProjectilePullForce { get; set; } - [Serialize(0.0f, false, description: "How much force is applied to pull the target the rope is attached to.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the target the rope is attached to.")] public float TargetPullForce { get; set; } - [Serialize(0.0f, false, description: "How much force is applied to pull the source the rope is attached to.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the source the rope is attached to.")] public float SourcePullForce { get; set; } - [Serialize(1000.0f, false, description: "How far the source item can be from the projectile until the rope breaks.")] + [Serialize(1000.0f, IsPropertySaveable.No, description: "How far the source item can be from the projectile until the rope breaks.")] public float MaxLength { get; set; } - [Serialize(true, false, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] + [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] public bool SnapOnCollision { get; set; } - [Serialize(true, false, description: "Should the rope snap when the character drops the aim?")] + [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when the character drops the aim?")] public bool SnapWhenNotAimed { get; set; } - [Serialize(30.0f, false, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")] + [Serialize(30.0f, IsPropertySaveable.No, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")] public float TargetMinMass { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool LerpForces { get; @@ -102,12 +102,12 @@ namespace Barotrauma.Items.Components } } - public Rope(Item item, XElement element) : base(item, element) + public Rope(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public void Snap() => Snapped = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs index 9c4998801..1d592e015 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs @@ -5,9 +5,9 @@ namespace Barotrauma.Items.Components { partial class Scanner : ItemComponent { - [Serialize(1.0f, false, description: "How long it takes for the scan to be completed.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How long it takes for the scan to be completed.")] public float ScanDuration { get; set; } - [Serialize(0.0f, false, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")] public float ScanTimer { get @@ -33,9 +33,9 @@ namespace Barotrauma.Items.Components #endif } } - [Serialize(1.0f, false, description: "How far the scanner can be from the target for the scan to be successful.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How far the scanner can be from the target for the scan to be successful.")] public float ScanRadius { get; set; } - [Serialize(true, false, description: "Should the progress bar always be displayed when the item has been attached.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the progress bar always be displayed when the item has been attached.")] public bool AlwaysDisplayProgressBar { get; set; } private Holdable Holdable { get; set; } @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components public Action OnScanStarted, OnScanCompleted; - public Scanner(Item item, XElement element) : base(item, element) + public Scanner(Item item, ContentXElement element) : base(item, element) { IsActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs index 6fa6e8f2c..cc0e5c4b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class AdderComponent : ArithmeticComponent { - public AdderComponent(Item item, XElement element) + public AdderComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index af4c47b13..22ceb46ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components protected readonly Character[] signalSender = new Character[2]; - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components } private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components } } - public AndComponent(Item item, XElement element) + public AndComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index ecf63774b..c31b4ff6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [Serialize(999999.0f, true, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), + [Serialize(999999.0f, IsPropertySaveable.Yes, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMax { @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(-999999.0f, true, description: "The output of the item is restricted above this value.", alwaysUseInstanceValues: true), + [Serialize(-999999.0f, IsPropertySaveable.Yes, description: "The output of the item is restricted above this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMin { @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components } [InGameEditable(DecimalCount = 2), - Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." + + Serialize(0.0f, IsPropertySaveable.Yes, description: "The item must have received signals to both inputs within this timeframe to output the result." + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components } } - public ArithmeticComponent(Item item, XElement element) + public ArithmeticComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index 38489ca15..b9dd02749 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -9,9 +9,9 @@ namespace Barotrauma.Items.Components { partial class ButtonTerminal : ItemComponent { - [Editable, Serialize(new string[0], true, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)] + [Editable, Serialize(new string[0], IsPropertySaveable.Yes, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)] public string[] Signals { get; set; } - [Editable, Serialize("", true, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)] + [Editable, Serialize("", IsPropertySaveable.Yes, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)] public string ActivatingItems { get; set; } private int RequiredSignalCount { get; set; } @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private bool AllowUsingButtons => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab))); - public ButtonTerminal(Item item, XElement element) : base(item, element) + public ButtonTerminal(Item item, ContentXElement element) : base(item, element) { RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); if (RequiredSignalCount < 1) @@ -31,7 +31,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void OnItemLoaded() { @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components } else { - ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t.Equals(activatingItem, StringComparison.OrdinalIgnoreCase))) + ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t == activatingItem)) .ForEach(p => ActivatingItemPrefabs.Add(p)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs index d3d41c271..fb610a6ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs @@ -11,10 +11,10 @@ namespace Barotrauma.Items.Components private string output = "0,0,0,0"; - [InGameEditable, Serialize(false, true, description: "When enabled makes the component translate the signal from HSV into RGB where red is the hue between 0 and 360, green is the saturation between 0 and 1 and blue is the value between 0 and 1.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled makes the component translate the signal from HSV into RGB where red is the hue between 0 and 360, green is the saturation between 0 and 1 and blue is the value between 0 and 1.", alwaysUseInstanceValues: true)] public bool UseHSV { get; set; } - public ColorComponent(Item item, XElement element) + public ColorComponent(Item item, ContentXElement element) : base(item, element) { receivedSignal = new float[4]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs index afcf91f2d..81ad65d84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { private int maxOutputLength; - [Editable, Serialize(256, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking load.")] + [Editable, Serialize(256, IsPropertySaveable.No, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking load.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -17,14 +17,14 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize("", false)] + [InGameEditable, Serialize("", IsPropertySaveable.No)] public string Separator { get; set; } - public ConcatComponent(Item item, XElement element) + public ConcatComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 185d8a158..f9c057603 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components public readonly int MaxWires = 5; public readonly string Name; - public readonly string DisplayName; + public readonly LocalizedString DisplayName; private readonly Wire[] wires; public IEnumerable Wires @@ -33,6 +33,12 @@ namespace Barotrauma.Items.Components public readonly ushort[] wireId; + //The grid the connection is a part of + public GridInfo Grid; + + //Priority in which power output will be handled - load is unaffected + public PowerPriority Priority = PowerPriority.Default; + public bool IsPower { get; @@ -40,7 +46,7 @@ namespace Barotrauma.Items.Components } private bool recipientsDirty = true; - private List recipients = new List(); + private readonly List recipients = new List(); public List Recipients { get @@ -66,17 +72,17 @@ namespace Barotrauma.Items.Components return "Connection (" + item.Name + ", " + Name + ")"; } - public Connection(XElement element, ConnectionPanel connectionPanel, IdRemap idRemap) + public Connection(ContentXElement element, ConnectionPanel connectionPanel, IdRemap idRemap) { #if CLIENT if (connector == null) { - connector = GUI.Style.GetComponentStyle("ConnectionPanelConnector").GetDefaultSprite(); - wireVertical = GUI.Style.GetComponentStyle("ConnectionPanelWire").GetDefaultSprite(); - connectionSprite = GUI.Style.GetComponentStyle("ConnectionPanelConnection").GetDefaultSprite(); - connectionSpriteHighlight = GUI.Style.GetComponentStyle("ConnectionPanelConnection").GetSprite(GUIComponent.ComponentState.Hover); - screwSprites = GUI.Style.GetComponentStyle("ConnectionPanelScrew").Sprites[GUIComponent.ComponentState.None].Select(s => s.Sprite).ToList(); + connector = GUIStyle.GetComponentStyle("ConnectionPanelConnector").GetDefaultSprite(); + wireVertical = GUIStyle.GetComponentStyle("ConnectionPanelWire").GetDefaultSprite(); + connectionSprite = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetDefaultSprite(); + connectionSpriteHighlight = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetSprite(GUIComponent.ComponentState.Hover); + screwSprites = GUIStyle.GetComponentStyle("ConnectionPanelScrew").Sprites[GUIComponent.ComponentState.None].Select(s => s.Sprite).ToList(); } #endif ConnectionPanel = connectionPanel; @@ -95,7 +101,7 @@ namespace Barotrauma.Items.Components //if displayname is not present, attempt to find it from the prefab if (element.Attribute("displayname") == null) { - foreach (XElement subElement in item.Prefab.ConfigElement.Elements()) + foreach (var subElement in item.Prefab.ConfigElement.Elements()) { if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -132,7 +138,7 @@ namespace Barotrauma.Items.Components } } - if (string.IsNullOrEmpty(DisplayName)) + if (DisplayName.IsNullOrEmpty()) { #if DEBUG DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name); @@ -145,7 +151,7 @@ namespace Barotrauma.Items.Components wireId = new ushort[MaxWires]; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -236,6 +242,34 @@ namespace Barotrauma.Items.Components var otherConnection = previousWire.OtherConnection(this); if (otherConnection != null) { + //Change the connection grids or flag them for updating + if (IsPower && otherConnection.IsPower && Grid != null) + { + //Check if both connections belong to a larger grid + if (otherConnection.recipients.Count > 1 && recipients.Count > 1) + { + Powered.ChangedConnections.Add(otherConnection); + Powered.ChangedConnections.Add(this); + } + else if (recipients.Count > 1) + { + //This wire was the only one at the other grid + otherConnection.Grid?.RemoveConnection(otherConnection); + otherConnection.Grid = null; + } + else if (otherConnection.recipients.Count > 1) + { + Grid?.RemoveConnection(this); + Grid = null; + } + else if (Grid.Connections.Count == 2) + { + //Delete the grid as these were the only 2 devices + Powered.Grids.Remove(Grid.ID); + Grid = null; + otherConnection.Grid = null; + } + } otherConnection.recipientsDirty = true; } } @@ -244,10 +278,32 @@ namespace Barotrauma.Items.Components recipientsDirty = true; if (wire != null) { + ConnectionPanel.DisconnectedWires.Remove(wire); var otherConnection = wire.OtherConnection(this); if (otherConnection != null) { + //Set the other connection grid if a grid exists already + if (Powered.ValidPowerConnection(this, otherConnection)) + { + if (Grid == null && otherConnection.Grid != null) + { + otherConnection.Grid.AddConnection(this); + Grid = otherConnection.Grid; + } + else if (Grid != null && otherConnection.Grid == null) + { + Grid.AddConnection(otherConnection); + otherConnection.Grid = Grid; + } + else + { + //Flag change so that proper grids can be formed + Powered.ChangedConnections.Add(this); + Powered.ChangedConnections.Add(otherConnection); + } + } + otherConnection.recipientsDirty = true; } } @@ -282,20 +338,17 @@ namespace Barotrauma.Items.Components } } - public void SendPowerProbeSignal(Item source, float power) - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null) { continue; } - - Connection recipient = wires[i].OtherConnection(this); - if (recipient == null || !recipient.IsPower) { continue; } - - recipient.item.GetComponent()?.ReceivePowerProbeSignal(recipient, source, power); - } - } public void ClearConnections() { + if (IsPower && Grid != null) + { + Powered.ChangedConnections.Add(this); + foreach (Connection c in recipients) + { + Powered.ChangedConnections.Add(c); + } + } + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 92103f633..48258b2ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -39,7 +40,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] public bool Locked { get; @@ -63,12 +64,12 @@ namespace Barotrauma.Items.Components get { return user; } } - public ConnectionPanel(Item item, XElement element) + public ConnectionPanel(Item item, ContentXElement element) : base(item, element) { Connections = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString()) { @@ -264,13 +265,13 @@ namespace Barotrauma.Items.Components return false; } - public override void Load(XElement element, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement element, bool usePrefabValues, IdRemap idRemap) { base.Load(element, usePrefabValues, idRemap); List loadedConnections = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString()) { @@ -306,7 +307,7 @@ namespace Barotrauma.Items.Components } } - disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", new ushort[0]).ToList(); + disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty()).ToList(); for (int i = 0; i < disconnectedWireIds.Count; i++) { disconnectedWireIds[i] = idRemap.GetOffsetId(disconnectedWireIds[i]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 8c8ed6036..fb9b68e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -14,13 +15,13 @@ namespace Barotrauma.Items.Components public string ConnectionName; public Connection Connection; - [Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] + [Serialize("", IsPropertySaveable.No, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } - [Serialize("1", false, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable] + [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 string PropertyName { get; } + public Identifier PropertyName { get; } public bool TargetOnlyParentProperty { get; } public int NumberInputMin { get; } @@ -35,7 +36,7 @@ namespace Barotrauma.Items.Components public string Name => "CustomInterfaceElement"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } public List StatusEffects = new List(); @@ -43,16 +44,16 @@ namespace Barotrauma.Items.Components /// Pass the parent component to the constructor to access the serializable properties /// for elements which change property values. /// - public CustomInterfaceElement(XElement element, CustomInterface parent) + public CustomInterfaceElement(Item item, ContentXElement element, CustomInterface parent) { Label = element.GetAttributeString("text", ""); ConnectionName = element.GetAttributeString("connection", ""); - PropertyName = element.GetAttributeString("propertyname", "").ToLowerInvariant(); + PropertyName = element.GetAttributeIdentifier("propertyname", ""); TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false); NumberInputMin = element.GetAttributeInt("min", DefaultNumberInputMin); NumberInputMax = element.GetAttributeInt("max", DefaultNumberInputMax); MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue); - HasPropertyName = !string.IsNullOrEmpty(PropertyName); + HasPropertyName = !PropertyName.IsEmpty; IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; if (element.Attribute("signal") is XAttribute attribute) @@ -84,7 +85,7 @@ namespace Barotrauma.Items.Components Signal = "1"; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase)) { @@ -95,7 +96,7 @@ namespace Barotrauma.Items.Components } private string[] labels; - [Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.", alwaysUseInstanceValues: true)] + [Serialize("", IsPropertySaveable.Yes, description: "The texts displayed on the buttons/tickboxes, separated by commas.", alwaysUseInstanceValues: true)] public string Labels { get { return string.Join(",", labels); } @@ -104,14 +105,14 @@ namespace Barotrauma.Items.Components if (value == null) { return; } if (customInterfaceElementList.Count > 0) { - string[] splitValues = value == "" ? new string[0] : value.Split(','); + string[] splitValues = value == "" ? Array.Empty() : value.Split(','); UpdateLabels(splitValues); } } } private string[] signals; - [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.", alwaysUseInstanceValues: true)] + [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) @@ -122,7 +123,7 @@ namespace Barotrauma.Items.Components if (value == null) { return; } if (customInterfaceElementList.Count > 0) { - string[] splitValues = value == "" ? new string[0] : value.Split(';'); + string[] splitValues = value == "" ? Array.Empty() : value.Split(';'); UpdateSignals(splitValues); } } @@ -132,17 +133,17 @@ namespace Barotrauma.Items.Components private readonly List customInterfaceElementList = new List(); - public CustomInterface(Item item, XElement element) + public CustomInterface(Item item, ContentXElement element) : base(item, element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "button": case "textbox": case "integerinput": - var button = new CustomInterfaceElement(subElement, this) + var button = new CustomInterfaceElement(item, subElement, this) { ContinuousSignal = false }; @@ -153,7 +154,7 @@ namespace Barotrauma.Items.Components customInterfaceElementList.Add(button); break; case "tickbox": - var tickBox = new CustomInterfaceElement(subElement, this) + var tickBox = new CustomInterfaceElement(item, subElement, this) { ContinuousSignal = true }; @@ -179,7 +180,7 @@ namespace Barotrauma.Items.Components labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label; if (Screen.Selected != GameMain.SubEditorScreen) { - customInterfaceElementList[i].Label = TextManager.Get(labels[i], returnNull: true) ?? labels[i]; + customInterfaceElementList[i].Label = TextManager.Get(labels[i]).Fallback(labels[i]).Value; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index c0c7c2872..d196c1b03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private DelayedSignal prevQueuedSignal; private float delay; - [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).", alwaysUseInstanceValues: true)] + [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, IsPropertySaveable.Yes, description: "How long the item delays the signals (in seconds).", alwaysUseInstanceValues: true)] public float Delay { get { return delay; } @@ -44,21 +44,21 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component discard previously received signals when a new one is received.", alwaysUseInstanceValues: true)] public bool ResetWhenSignalReceived { get; set; } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component discard previously received signals when the incoming signal changes.", alwaysUseInstanceValues: true)] public bool ResetWhenDifferentSignalReceived { get; set; } - public DelayComponent(Item item, XElement element) + public DelayComponent(Item item, ContentXElement element) : base (item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs index e4efa15d5..8c35c711e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class DivideComponent : ArithmeticComponent { - public DivideComponent(Item item, XElement element) + public DivideComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 4ffc063f7..892a3afef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components protected float timeFrame; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } @@ -72,7 +72,7 @@ namespace Barotrauma.Items.Components } } - public EqualsComponent(Item item, XElement element) + public EqualsComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs index 8ea0ca87b..d2eeddbad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class ExponentiationComponent : ItemComponent { private float exponent; - [InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(1.0f, IsPropertySaveable.No, description: "The exponent of the operation.", alwaysUseInstanceValues: true)] public float Exponent { get @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components } } - public ExponentiationComponent(Item item, XElement element) + public ExponentiationComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index 8d464e70a..974cac797 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -16,13 +16,13 @@ namespace Barotrauma.Items.Components SquareRoot } - [Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] + [Serialize(FunctionType.Round, IsPropertySaveable.No, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; } - public FunctionComponent(Item item, XElement element) + public FunctionComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs index 0f15476c1..864046385 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { private float val1, val2; - public GreaterComponent(Item item, XElement element) + public GreaterComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index c11c33e5f..ef5d693af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private Turret turret; - [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), + [Serialize(100.0f, IsPropertySaveable.Yes, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { @@ -56,7 +56,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, true, description: "Should structures cast shadows when light from this light source hits them. " + + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should structures cast shadows when light from this light source hits them. " + "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.", alwaysUseInstanceValues: true)] public bool CastShadows { @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Lights drawn behind submarines don't cast any shadows and are much faster to draw than shadow-casting lights. " + + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Lights drawn behind submarines don't cast any shadows and are much faster to draw than shadow-casting lights. " + "It's recommended to enable this on decorative lights outside the submarine's hull.", alwaysUseInstanceValues: true)] public bool DrawBehindSubs { @@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Is the light currently on.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Is the light currently on.", alwaysUseInstanceValues: true)] public bool IsOn { get { return isOn; } @@ -98,7 +98,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] public float Flicker { get { return flicker; } @@ -111,7 +111,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] + [Editable, Serialize(1.0f, IsPropertySaveable.No, description: "How fast the light flickers.")] public float FlickerSpeed { get { return flickerSpeed; } @@ -124,7 +124,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(0.0f, true, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] public float PulseFrequency { get { return pulseFrequency; } @@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components } } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, true, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] public float PulseAmount { get { return pulseAmount; } @@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] public float BlinkFrequency { get { return blinkFrequency; } @@ -163,7 +163,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable(FallBackTextTag = "connection.setcolor"), Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).", alwaysUseInstanceValues: true)] + [InGameEditable(FallBackTextTag = "connection.setcolor"), Serialize("255,255,255,255", IsPropertySaveable.Yes, description: "The color of the emitted light (R,G,B,A).", alwaysUseInstanceValues: true)] public Color LightColor { get { return lightColor; } @@ -179,7 +179,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "If enabled, the component will ignore continuous signals received in the toggle input (i.e. a continuous signal will only toggle it once).")] + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the component will ignore continuous signals received in the toggle input (i.e. a continuous signal will only toggle it once).")] public bool IgnoreContinuousToggle { get; @@ -208,7 +208,7 @@ namespace Barotrauma.Items.Components } } - public LightComponent(Item item, XElement element) + public LightComponent(Item item, ContentXElement element) : base(item, element) { #if CLIENT @@ -264,8 +264,6 @@ namespace Barotrauma.Items.Components } UpdateOnActiveEffects(deltaTime); - if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; } - #if CLIENT Light.ParentSub = item.Submarine; #endif @@ -284,7 +282,7 @@ namespace Barotrauma.Items.Components return; } - currPowerConsumption = powerConsumption; + //currPowerConsumption = powerConsumption; if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage)) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index 39ee77a58..6fc568545 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components partial class MemoryComponent : ItemComponent, IServerSerializable { private int maxValueLength; - [Editable, Serialize(200, false, description: "The maximum length of the stored value. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the stored value. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxValueLength { get { return maxValueLength; } @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components private string value; - [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] public string Value { get { return value; } @@ -34,14 +34,14 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, true, description: "Can the value stored in the memory component be changed via signals.", alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the value stored in the memory component be changed via signals.", alwaysUseInstanceValues: true)] public bool Writeable { get; set; } - public MemoryComponent(Item item, XElement element) + public MemoryComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs index 2d8985857..aa7bcfdcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class ModuloComponent : ItemComponent { private float modulus; - [InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(1.0f, IsPropertySaveable.No, description: "The modulus of the operation. Must be non-zero.", alwaysUseInstanceValues: true)] public float Modulus { get { return modulus; } @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components } } - public ModuloComponent(Item item, XElement element) : base(item, element) + public ModuloComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 3c4db92a5..111ed4ea2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -22,17 +22,17 @@ namespace Barotrauma.Items.Components Wall } - [Serialize(false, false, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] + [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] public bool MotionDetected { get; set; } - [InGameEditable, Serialize(TargetType.Any, true, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)] public TargetType Target { get; set; } - [InGameEditable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] public bool IgnoreDead { get; @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } - [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Horizontal detection range.", alwaysUseInstanceValues: true)] public float RangeX { get { return rangeX; } @@ -52,7 +52,7 @@ namespace Barotrauma.Items.Components #endif } } - [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)] public float RangeY { get { return rangeY; } @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("0,0", true, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")] + [InGameEditable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")] public Vector2 DetectOffset { get { return detectOffset; } @@ -85,7 +85,7 @@ namespace Barotrauma.Items.Components } } - [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, true, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)] + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)] public float UpdateInterval { get; @@ -93,7 +93,7 @@ namespace Barotrauma.Items.Components } private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -104,7 +104,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -120,7 +120,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -135,21 +135,21 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [Editable(DecimalCount = 3), Serialize(0.01f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; set; } - [Serialize(true, true, description: "Should the sensor trigger when the item itself moves.")] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the sensor trigger when the item itself moves.")] public bool DetectOwnMotion { get; set; } - public MotionSensor(Item item, XElement element) + public MotionSensor(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -164,7 +164,7 @@ namespace Barotrauma.Items.Components updateTimer = Rand.Range(0.0f, UpdateInterval); } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); //backwards compatibility diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs index 5f671aa21..69d75613f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class MultiplyComponent : ArithmeticComponent { - public MultiplyComponent(Item item, XElement element) + public MultiplyComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs index 99e3cc3e2..6f6059251 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs @@ -7,14 +7,14 @@ namespace Barotrauma.Items.Components private bool signalReceived; private bool continuousOutput; - [Editable, Serialize(false, true, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get { return continuousOutput; } set { continuousOutput = IsActive = value; } } - public NotComponent(Item item, XElement element) + public NotComponent(Item item, ContentXElement element) : base (item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs index 8596b8070..3d3c7ab9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class OrComponent : AndComponent { - public OrComponent(Item item, XElement element) + public OrComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs index 6d92474fe..4c4493ead 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components private float phase; - [InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." + + [InGameEditable, Serialize(WaveType.Pulse, IsPropertySaveable.Yes, description: "What kind of a signal the item outputs." + " Pulse: periodically sends out a signal of 1." + " Sawtooth: sends out a periodic wave that increases linearly from 0 to 1." + " Sine: sends out a sine wave oscillating between -1 and 1." + @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components set; } - [InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 2), Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).", alwaysUseInstanceValues: true)] public float Frequency { get { return frequency; } @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components } } - public OscillatorComponent(Item item, XElement element) : + public OscillatorComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs index 9cc1020e7..146019cdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs @@ -4,10 +4,12 @@ namespace Barotrauma.Items.Components { class OxygenDetector : ItemComponent { + public const int LowOxygenPercentage = 35; + private int prevSentOxygenValue; private string oxygenSignal; - public OxygenDetector(Item item, XElement element) + public OxygenDetector(Item item, ContentXElement element) : base (item, element) { IsActive = true; @@ -17,13 +19,15 @@ namespace Barotrauma.Items.Components { if (item.CurrentHull == null) { return; } - if (prevSentOxygenValue != (int)item.CurrentHull.OxygenPercentage || oxygenSignal == null) + int currOxygenPercentage = (int)item.CurrentHull.OxygenPercentage; + if (prevSentOxygenValue != currOxygenPercentage || oxygenSignal == null) { - prevSentOxygenValue = (int)item.CurrentHull.OxygenPercentage; + prevSentOxygenValue = currOxygenPercentage; oxygenSignal = prevSentOxygenValue.ToString(); } - item.SendSignal(oxygenSignal, "signal_out"); + item.SendSignal(oxygenSignal, "signal_out"); + item.SendSignal(currOxygenPercentage <= LowOxygenPercentage ? "1" : "0", "low_oxygen"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index 8e7ea8674..f23292212 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private bool nonContinuousOutputSent; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components private string output; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -48,16 +48,16 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] public bool UseCaptureGroup { get; set; } - [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(true, IsPropertySaveable.Yes, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get; set; } - [InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The regular expression used to check the incoming signals.", alwaysUseInstanceValues: true)] public string Expression { get { return expression; } @@ -82,7 +82,7 @@ namespace Barotrauma.Items.Components } } - public RegExFindComponent(Item item, XElement element) + public RegExFindComponent(Item item, ContentXElement element) : base(item, element) { nonContinuousOutputSent = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 138277cee..9ade22e1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -13,7 +13,23 @@ namespace Barotrauma.Items.Components private bool isOn; - private float throttlePowerOutput; + private float prevVoltage; + + private float? newVoltage = null; + + //internal load buffer that's used to isolate the input and output sides from each other + private float internalLoadBuffer = 0; + + //previous load (used to smooth changes in the load to prevent oscillations) + private float prevInternalLoad = 0; + + //previous load on the output side + private float prevExternalLoad = 0; + + //difference between the internal load buffer and external load + private float bufferDiff = 0; + + private float thirdInverseMax = 0, loadEqnConstant = 0; private static readonly Dictionary connectionPairs = new Dictionary { @@ -25,19 +41,36 @@ namespace Barotrauma.Items.Components { "signal_in4", "signal_out4" }, { "signal_in5", "signal_out5" } }; - public float DisplayLoad { get; set; } - [Editable, Serialize(1000.0f, true, description: "The maximum amount of power that can pass through the item.")] + protected override PowerPriority Priority { get { return PowerPriority.Relay; } } + + public float DisplayLoad + { + get + { + if (powerOut != null && powerOut.Grid != null) + { + return powerOut.Grid.Load; + } + else + { + return 0; + } + } + } + + [Editable, Serialize(1000.0f, IsPropertySaveable.Yes, description: "The maximum amount of power that can pass through the item.")] public float MaxPower { get { return maxPower; } set { maxPower = Math.Max(0.0f, value); + SetLoadFormulaValues(); } } - [Editable, Serialize(true, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] public bool IsOn { get @@ -55,13 +88,23 @@ namespace Barotrauma.Items.Components } } - public RelayComponent(Item item, XElement element) + public RelayComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; - throttlePowerOutput = MaxPower; + prevVoltage = 0; + SetLoadFormulaValues(); } + private void SetLoadFormulaValues() + { + internalLoadBuffer = MaxPower * 2; + // Set constants for load Formula to reduce calculation time + thirdInverseMax = 1 / (3 * maxPower); + loadEqnConstant = (8 * maxPower) / 3; + } + + public override void OnItemLoaded() { base.OnItemLoaded(); @@ -87,8 +130,8 @@ namespace Barotrauma.Items.Components RefreshConnections(); item.SendSignal(IsOn ? "1" : "0", "state_out"); - - if (!CanTransfer) { Voltage = 0.0f; return; } + item.SendSignal(((int)Math.Round(-PowerLoad)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(DisplayLoad)).ToString(), "load_value_out"); if (isBroken) { @@ -98,75 +141,199 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerOut != null) - { - bool overloaded = false; - foreach (Connection recipient in powerOut.Recipients) - { - var pt = recipient.Item.GetComponent(); - if (pt != null) - { - float overload = -pt.CurrPowerConsumption - pt.PowerLoad; - throttlePowerOutput += overload * deltaTime * 0.5f; - overloaded = overload > 1.0f; - } - } - throttlePowerOutput = overloaded ? - MathHelper.Clamp(throttlePowerOutput, 0.0f, MaxPower): - Math.Max(throttlePowerOutput - MaxPower * 0.1f * deltaTime, 0.0f); - } - - if (Math.Min(-currPowerConsumption, PowerLoad) > maxPower && CanBeOverloaded) + if (Voltage > OverloadVoltage && CanBeOverloaded) { item.Condition = 0.0f; } } - public override void ReceivePowerProbeSignal(Connection connection, Item source, float power) + /// + /// Relay power consumption. Load consumption is based on the internal buffer. + /// This allows for the relay to react to demand and find equilibrium in loop configurations. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) { - if (!IsOn || item.Condition <= 0.0f) { return; } - - //we've already received this signal - if (lastPowerProbeRecipients.Contains(this)) { return; } - lastPowerProbeRecipients.Add(this); - - if (power < 0.0f) + //Can't output or draw if broken + if (isBroken) { - if (!connection.IsOutput || powerIn == null) { return; } + return 0; + } - //power being drawn from the power_out connection - DisplayLoad -= Math.Min(power, 0.0f); - powerLoad -= Math.Min(power + throttlePowerOutput, 0.0f); + if (connection == powerIn) + { + float currentLoad = MaxPower; + if (internalLoadBuffer > MaxPower) + { + //Buffer load charging curve - special relay sauce + // Original formula (buffer - 3 * maxPower)^2 / (3 * maxPower) - (maxPower / 3) + //loadDraw = MathHelper.Clamp((float)Math.Pow(internalBuffer - 3 * MaxPower, 2) / (3 * MaxPower) - MaxPower / 3, 1, MaxPower); + //Optimised formula 0.2% error from original + currentLoad = MathHelper.Clamp(internalLoadBuffer * internalLoadBuffer * thirdInverseMax - 2 * internalLoadBuffer + loadEqnConstant, 0.001f, MaxPower); - //pass the load to items connected to the input - powerIn.SendPowerProbeSignal(source, Math.Max(power, -MaxPower)); + //Slight smoothing to load to minimise relay jank + currentLoad = MathHelper.Clamp((currentLoad + prevInternalLoad * 0.1f) / 1.1f, 0.001f, MaxPower); + prevInternalLoad = currentLoad; + } + + //Add on extra load after calculation + currentLoad += ExtraLoad; + return currentLoad; } else { - if (connection.IsOutput || powerOut == null) { return; } - //power being supplied to the power_in connection - if (currPowerConsumption - power < -MaxPower) + //Flag output as power out + return -1; + } + } + + private bool RelayCanOutput() + { + //Only allow output if device is on, buffers have charge and the connected grids aren't short circuited + return isOn && powerIn != null && powerIn.Grid != null && internalLoadBuffer > 0 && powerOut != null && powerOut.Grid != null && powerIn.Grid != powerOut.Grid; + } + + /// + /// Minimum and maximum power out for the relay. + /// Max out is adjusted to allow for other relays to compensate if this relay is undervolted. + /// + public override PowerRange MinMaxPowerOut(Connection connection, float load = 0) + { + if (connection == powerOut) + { + if (RelayCanOutput()) { - power += MaxPower + (currPowerConsumption - power); - } + //Determine output limits from buffer and voltage + float bufferDraw = MathHelper.Min(internalLoadBuffer, MaxPower); + float voltageLimit = MathHelper.Min(MaxPower, load) * prevVoltage; - currPowerConsumption -= power; + //If undervolted adjust max output so that other relays can compensate + if (prevVoltage < 1) + { + voltageLimit *= prevVoltage; + } - foreach (Connection recipient in powerOut.Recipients) - { - if (!recipient.IsPower) { continue; } - var powered = recipient.Item.GetComponent(); - if (powered == null) { continue; } - - float load = powered.CurrPowerConsumption; - var powerTransfer = powered as PowerTransfer; - if (powerTransfer != null) { load = powerTransfer.PowerLoad; } - - float powerOut = power * (load / Math.Max(powerLoad + throttlePowerOutput, 0.01f)); - powered.ReceivePowerProbeSignal(recipient, source, Math.Min(powerOut, power)); + float maxOutput = MathHelper.Min(voltageLimit, bufferDraw); + return new PowerRange(0.0f, maxOutput); } } + + return PowerRange.Zero; + } + + /// + /// Power out for the relay connection. + /// Relay will output the necessary power to the grid based on maximum power output of other + /// relays and will undervolt and overvolt the grid following its supply grid. + /// + /// Power outputted to the grid + public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) + { + if (connection == powerIn) + { + return 0.0f; + } + else if (RelayCanOutput()) + { + //Determine output limits of the relay + float bufferDraw = MathHelper.Min(internalLoadBuffer, MaxPower); + float voltageLimit = MathHelper.Min(MaxPower, load) * prevVoltage; + float maxOut = MathHelper.Min(voltageLimit, bufferDraw); + + //Don't output negative power to the grid + if (maxOut < 0) + { + PowerLoad = 0; + return 0; + } + + prevExternalLoad = load; + + //Calculate power out + PowerLoad = MathHelper.Clamp((load * prevVoltage - power) / MathHelper.Max(minMaxPower.Max, 1E-20f) * -maxOut, -maxOut, 0); + return -PowerLoad; + } + //Else relay isn't outputting + PowerLoad = 0; + return 0; + } + + /// + /// Connection's grid resolved, determine the difference to be added to the buffer. + /// Ensure the prevVoltage voltage is updated once both grids are resolved. + /// + public override void GridResolved(Connection conn) + { + if (conn == powerIn) + { + if (powerIn != null && powerIn.Grid != null) + { + float addToBuffer = powerIn.Grid.Voltage * (CurrPowerConsumption - ExtraLoad); + + //Limit power input to the previous voltage to prevent wild oscillations in overload + if (powerIn.Grid.Voltage > 1) + { + addToBuffer = prevVoltage * (CurrPowerConsumption - ExtraLoad); + } + + //Cap the max power input + if (addToBuffer > MaxPower) + { + addToBuffer = MaxPower; + } + + //To prevent problems with grid order, only update voltage and buffer after input and output grids have been resolved + if (newVoltage == null) + { + //temporarily store the new voltage and also indicates that the input connection side has been updated + newVoltage = powerIn.Grid.Voltage; + bufferDiff = addToBuffer; + } + else + { + UpdateBuffer(addToBuffer, powerIn.Grid.Voltage); + } + } + } + else + { + //To prevent problems with grid order, only update voltage and buffer after input and output grids have been resolved + if (newVoltage == null) + { + //Flag that output connection has been updated already + newVoltage = -1; + bufferDiff = PowerLoad; + } + else + { + UpdateBuffer(PowerLoad, (float)newVoltage); + } + } + } + + private void UpdateBuffer(float addToBuffer, float newVoltage) + { + //Update buffer and voltage + float limit = MaxPower * 2; + + //Clamp the buffer to have a constant load in a severe overload event, otherwise wild oscillation will occur + if (RelayCanOutput() && powerIn.Grid.Voltage > 2) + { + limit = MathHelper.Min(limit, 3 * MaxPower - (float)Math.Sqrt((3 * prevExternalLoad + MaxPower) * MaxPower)); + } + + //Add to the internal buffer + internalLoadBuffer = MathHelper.Clamp(internalLoadBuffer + bufferDiff + addToBuffer, 0, limit); + + //Decay overvoltage slightly, helps resolve large chain loops to a grid + if (newVoltage > 1) + { + newVoltage = MathHelper.Max(newVoltage - 0.0005f, 1); + } + + prevVoltage = newVoltage; + this.newVoltage = null; + bufferDiff = 0; } public override void ReceiveSignal(Signal signal, Connection connection) @@ -192,7 +359,7 @@ namespace Barotrauma.Items.Components public void SetState(bool on, bool isNetworkMessage) { #if CLIENT - if (GameMain.Client != null && !isNetworkMessage) return; + if (GameMain.Client != null && !isNetworkMessage) { return; } #endif #if SERVER @@ -210,7 +377,7 @@ namespace Barotrauma.Items.Components msg.Write(isOn); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float _) { SetState(msg.ReadBoolean(), true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs index 9cc306e6a..34d856955 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class SignalCheckComponent : ItemComponent { private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -48,10 +48,10 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)] public string TargetSignal { get; set; } - public SignalCheckComponent(Item item, XElement element) + public SignalCheckComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs index 1a924aa6f..22cbef2ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components private bool fireInRange; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected a fire.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item outputs when it has detected a fire.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected a fire.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected a fire.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -53,7 +53,7 @@ namespace Barotrauma.Items.Components } } - public SmokeDetector(Item item, XElement element) + public SmokeDetector(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs index 5333bec84..a8c69fdeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components [InGameEditable(DecimalCount = 2), - Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." + + Serialize(0.0f, IsPropertySaveable.Yes, description: "The item must have received signals to both inputs within this timeframe to output the result." + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components } } - public StringComponent(Item item, XElement element) + public StringComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs index c4a870661..ed7dcc8d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class SubtractComponent : ArithmeticComponent { - public SubtractComponent(Item item, XElement element) + public SubtractComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 22fd92e52..13409a48e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -32,14 +32,14 @@ namespace Barotrauma.Items.Components private List messageHistory = new List(MaxMessages); - public string DisplayedWelcomeMessage + public LocalizedString DisplayedWelcomeMessage { get; private set; } private string welcomeMessage; - [InGameEditable, Serialize("", true, "Message to be displayed on the terminal display when it is first opened.", translationTextTag = "terminalwelcomemsg.", AlwaysUseInstanceValues = true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, "Message to be displayed on the terminal display when it is first opened.", translationTextTag: "terminalwelcomemsg.", alwaysUseInstanceValues: true)] public string WelcomeMessage { get { return welcomeMessage; } @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components { if (welcomeMessage == value) { return; } welcomeMessage = value; - DisplayedWelcomeMessage = TextManager.Get(welcomeMessage, returnNull: true) ?? welcomeMessage.Replace("\\n", "\n"); + DisplayedWelcomeMessage = TextManager.Get(welcomeMessage).Fallback(welcomeMessage.Replace("\\n", "\n")); } } @@ -64,12 +64,12 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] public bool UseMonospaceFont { get; set; } private Color textColor = Color.LimeGreen; - [Editable, Serialize("50,205,50,255", true, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] + [Editable, Serialize("50,205,50,255", IsPropertySaveable.Yes, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] public Color TextColor { get => textColor; @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components private string prevColorSignal; - public Terminal(Item item, XElement element) + public Terminal(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -143,9 +143,9 @@ namespace Barotrauma.Items.Components #endif base.OnItemLoaded(); - if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) + if (!DisplayedWelcomeMessage.IsNullOrEmpty()) { - ShowOnDisplay(DisplayedWelcomeMessage, addToHistory: !isSubEditor, TextColor); + ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor); DisplayedWelcomeMessage = ""; //remove welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) @@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components return componentElement; } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); for (int i = 0; i < MaxMessages; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 6814915aa..b16dce24c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -20,21 +20,21 @@ namespace Barotrauma.Items.Components private readonly float[] receivedSignal = new float[2]; private readonly float[] timeSinceReceived = new float[2]; - [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] + [Serialize(FunctionType.Sin, IsPropertySaveable.No, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; } - [InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the trigonometric function uses radians instead of degrees.", alwaysUseInstanceValues: true)] public bool UseRadians { get; set; } - public TrigonometricFunctionComponent(Item item, XElement element) + public TrigonometricFunctionComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 6d78cea05..7fc471035 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components private float stateSwitchDelay; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components } } - public WaterDetector(Item item, XElement element) + public WaterDetector(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index ec74d3894..c95c55497 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -30,10 +30,10 @@ namespace Barotrauma.Items.Components private Connection signalInConnection; private Connection signalOutConnection; - [Serialize(CharacterTeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] public CharacterTeamType TeamID { get; set; } - [Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)] + [Editable, Serialize(20000.0f, IsPropertySaveable.No, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)] public float Range { get { return range; } @@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(0, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(0, IsPropertySaveable.Yes, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] public int Channel { get { return channel; } @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components } - [Editable, Serialize(false, true, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)] public bool AllowCrossTeamCommunication { get; @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components } [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat)] - [Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + + [Serialize(false, IsPropertySaveable.No, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + "as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)] public bool LinkToChat { @@ -73,7 +73,7 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(1.0f, true, description: "How many seconds have to pass between signals for a message to be displayed in the chatbox. " + + [Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "How many seconds have to pass between signals for a message to be displayed in the chatbox. " + "Setting this to a very low value is not recommended, because it may cause an excessive amount of chat messages to be created " + "if there are chat-linked wifi components that transmit a continuous signal.")] public float MinChatMessageInterval @@ -82,14 +82,14 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(false, true, description: "If set to true, the component will only create chat messages when the received signal changes.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the component will only create chat messages when the received signal changes.")] public bool DiscardDuplicateChatMessages { get; set; } - public WifiComponent(Item item, XElement element) + public WifiComponent(Item item, ContentXElement element) : base (item, element) { list.Add(this); @@ -194,8 +194,6 @@ namespace Barotrauma.Items.Components { item.LastSentSignalRecipients.Clear(); } - var senderComponent = signal.source?.GetComponent(); - if (senderComponent != null && !CanReceive(senderComponent)) { return; } bool chatMsgSent = false; @@ -247,7 +245,7 @@ namespace Barotrauma.Items.Components wifiComp.item.ParentInventory.Owner != null) { string chatMsg = signal.value; - if (senderComponent != null) + if (sentSignalStrength <= 1.0f) { chatMsg = ChatMessage.ApplyDistanceEffect(chatMsg, 1.0f - sentSignalStrength); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index ec7a0db91..ee21d0939 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -81,35 +81,35 @@ namespace Barotrauma.Items.Components get { return connections; } } - [Serialize(5000.0f, false, description: "The maximum distance the wire can extend (in pixels).")] + [Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")] public float MaxLength { get; set; } - [Serialize(false, false, description: "If enabled, the wire will not be visible in connection panels outside the submarine editor.")] + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the wire will not be visible in connection panels outside the submarine editor.")] public bool HiddenInGame { get; set; } - [Editable, Serialize(false, true, "If enabled, this wire will be ignored by the \"Lock all default wires\" setting.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, "If enabled, this wire will be ignored by the \"Lock all default wires\" setting.", alwaysUseInstanceValues: true)] public bool NoAutoLock { get; set; } - [Editable, Serialize(false, true, "If enabled, this wire will use the sprite depth instead of a constant depth.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, "If enabled, this wire will use the sprite depth instead of a constant depth.")] public bool UseSpriteDepth { get; set; } - public Wire(Item item, XElement element) + public Wire(Item item, ContentXElement element) : base(item, element) { nodes = new List(); @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public Connection OtherConnection(Connection connection) { @@ -785,7 +785,7 @@ namespace Barotrauma.Items.Components UpdateSections(); } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs index bf493f470..71981bb8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class XorComponent : AndComponent { - public XorComponent(Item item, XElement element) + public XorComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs index 833b66c2e..5e46bd13c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class StatusHUD : ItemComponent { - public StatusHUD(Item item, XElement element) + public StatusHUD(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index a748459c7..582357d8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components { partial class TriggerComponent : ItemComponent { - [Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] public float Force { get; set; } public PhysicsBody PhysicsBody { get; private set; } @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components /// private readonly List attacks = new List(); - public TriggerComponent(Item item, XElement element) : base(item, element) + public TriggerComponent(Item item, ContentXElement element) : base(item, element) { string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); if (!Enum.TryParse(triggeredByAttribute, out triggeredBy)) @@ -72,7 +72,7 @@ namespace Barotrauma.Items.Components forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f); string parentDebugName = $"TriggerComponent in {item.Name}"; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 0de5ab645..ca3398fee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components get { return rotation; } } - [Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] + [Serialize("0,0", IsPropertySaveable.No, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] public Vector2 BarrelPos { get @@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components } } - [Serialize("0,0", false, description: "The projectile launching location relative to transformed barrel position (in pixels).")] + [Serialize("0,0", IsPropertySaveable.No, description: "The projectile launching location relative to transformed barrel position (in pixels).")] public Vector2 FiringOffset { get; @@ -106,49 +106,49 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, false, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched).")] public float LaunchImpulse { get { return launchImpulse; } set { launchImpulse = value; } } - [Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")] + [Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, IsPropertySaveable.No, description: "The period of time the user has to wait between shots.")] public float Reload { get { return reloadTime; } set { reloadTime = value; } } - [Editable(0.1f, 10f), Serialize(1.0f, false, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")] + [Editable(0.1f, 10f), Serialize(1.0f, IsPropertySaveable.No, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")] public float RetractionDurationMultiplier { get; set; } - [Editable(0.1f, 10f), Serialize(0.1f, false, description: "How quickly the recoil moves the barrel after launching.")] + [Editable(0.1f, 10f), Serialize(0.1f, IsPropertySaveable.No, description: "How quickly the recoil moves the barrel after launching.")] public float RecoilTime { get; set; } - [Editable(0f, 1000f), Serialize(0f, false, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")] + [Editable(0f, 1000f), Serialize(0f, IsPropertySaveable.No, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")] public float RetractionDelay { get; set; } - [Serialize(1, false, description: "How many projectiles the weapon launches when fired once.")] + [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { get; set; } - [Serialize(false, false, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")] + [Serialize(false, IsPropertySaveable.No, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")] public bool LaunchWithoutProjectile { get; @@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components } [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), - Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] + Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] public Vector2 RotationLimits { get @@ -179,7 +179,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles (in degrees).")] public float Spread { get; @@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(5.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character" + Serialize(5.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character" + " with insufficient skills to operate it. Higher values make the barrel rotate faster.")] public float SpringStiffnessLowSkill { @@ -195,7 +195,7 @@ namespace Barotrauma.Items.Components private set; } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(2.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character" + Serialize(2.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character" + " with sufficient skills to operate it. Higher values make the barrel rotate faster.")] public float SpringStiffnessHighSkill { @@ -204,7 +204,7 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(50.0f, false, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + Serialize(50.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + " with insufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")] public float SpringDampingLowSkill { @@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components private set; } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(10.0f, false, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + Serialize(10.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + " with sufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")] public float SpringDampingHighSkill { @@ -221,28 +221,28 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 100.0f, DecimalCount = 2), - Serialize(1.0f, false, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it.")] + Serialize(1.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it.")] public float RotationSpeedLowSkill { get; private set; } [Editable(0.0f, 100.0f, DecimalCount = 2), - Serialize(5.0f, false, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),] + Serialize(5.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),] public float RotationSpeedHighSkill { get; private set; } - [Serialize(1.0f, false, description: "How fast the turret can rotate while firing (for charged weapons).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the turret can rotate while firing (for charged weapons).")] public float FiringRotationSpeedModifier { get; private set; } - [Serialize(false, true, description: "Whether the turret should always charge-up fully to shoot.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Whether the turret should always charge-up fully to shoot.")] public bool SingleChargedShot { get; @@ -251,7 +251,7 @@ namespace Barotrauma.Items.Components private float prevScale; float prevBaseRotation; - [Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] public float BaseRotation { get { return item.Rotation; } @@ -262,33 +262,33 @@ namespace Barotrauma.Items.Components } } - [Serialize(3000.0f, true, description: "How close to a target the turret has to be for an AI character to fire it.")] + [Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")] public float AIRange { get; set; } - [Serialize(-1, true, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] + [Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] public int MaxActiveProjectiles { get; set; } - [Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")] + [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] public float MaxChargeTime { get; private set; } - public Turret(Item item, XElement element) + public Turret(Item item, ContentXElement element) : base(item, element) { IsActive = true; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -315,7 +315,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); private void UpdateTransformedBarrelPos() { @@ -456,7 +456,7 @@ namespace Barotrauma.Items.Components // Do not increase the weapons skill when operating a turret in an outpost level if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost)) { - user.Info.IncreaseSkillLevel("weapons", + user.Info.IncreaseSkillLevel("weapons".ToIdentifier(), SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f)); } @@ -601,7 +601,7 @@ namespace Barotrauma.Items.Components //use linked projectile containers in case they have to react to the turret being launched somehow //(play a sound, spawn more projectiles) if (!(e is Item linkedItem)) { continue; } - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.Condition <= 0.0f) { loaderBroken = true; @@ -644,7 +644,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { if (!(e is Item linkedItem)) { continue; } - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) { tinkeringStrength = repairable.TinkeringStrength; @@ -653,8 +653,9 @@ namespace Barotrauma.Items.Components if (!ignorePower) { - var batteries = item.GetConnectedComponents(); + List batteries = GetConnectedBatteries(); float neededPower = GetPowerRequiredToShoot(); + // tinkering is currently not factored into the common method as it is checked only when shooting // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); @@ -864,8 +865,8 @@ namespace Barotrauma.Items.Components foreach (var character in Character.CharacterList) { if (character == null || character.Removed || character.IsDead) { continue; } - if (character.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) { continue; } - bool isHuman = character.IsHuman || character.Params.Group.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); + if (character.Params.Group == ai.Config.Entity) { continue; } + bool isHuman = character.IsHuman || character.Params.Group == CharacterPrefab.HumanSpeciesName; if (isHuman) { if (!targetHumans) @@ -901,7 +902,7 @@ namespace Barotrauma.Items.Components closestDist = shootDistance * shootDistance; if (closestSub != null) { - foreach (var hull in Hull.hullList) + foreach (var hull in Hull.HullList) { if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); @@ -984,8 +985,8 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { - character.Speak(TextManager.Get("DialogTurretTargetDead"), - identifier: "killedtarget" + previousTarget.ID, + character.Speak(TextManager.Get("DialogTurretTargetDead").Value, + identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(), minDurationBetweenSimilar: 10.0f); character.AIController.SelectTarget(null); } @@ -993,7 +994,7 @@ namespace Barotrauma.Items.Components bool canShoot = true; if (!HasPowerToShoot()) { - var batteries = item.GetConnectedComponents(); + List batteries = GetConnectedBatteries(); float lowestCharge = 0.0f; PowerContainer batteryToLoad = null; foreach (PowerContainer battery in batteries) @@ -1013,8 +1014,8 @@ namespace Barotrauma.Items.Components } else { - character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"), - identifier: "supercapacitorisbroken", + character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value, + identifier: "supercapacitorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); canShoot = false; } @@ -1023,13 +1024,13 @@ namespace Barotrauma.Items.Components if (batteryToLoad == null) { return true; } if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f) { - objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: "", requireEquip: false)); + objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: Identifier.Empty, requireEquip: false)); return false; } if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0) { - character.Speak(TextManager.Get("DialogTurretHasNoPower"), - identifier: "turrethasnopower", + character.Speak(TextManager.Get("DialogTurretHasNoPower").Value, + identifier: "turrethasnopower".ToIdentifier(), minDurationBetweenSimilar: 30.0f); canShoot = false; } @@ -1040,7 +1041,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { if (!item.IsInteractable(character)) { continue; } - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (e is Item projectileContainer) { var container = projectileContainer.GetComponent(); @@ -1070,8 +1071,8 @@ namespace Barotrauma.Items.Components { if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true), - identifier: "cannotloadturret", + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value, + identifier: "cannotloadturret".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } return true; @@ -1079,11 +1080,11 @@ namespace Barotrauma.Items.Components if (objective.SubObjectives.None()) { var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true); - loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; + loadItemsObjective.ignoredContainerIdentifiers = new Identifier[] { ((MapEntity)containerItem).Prefab.Identifier }; if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true), - identifier: "loadturret", + character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value, + identifier: "loadturret".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } loadItemsObjective.Abandoned += CheckRemainingAmmo; @@ -1094,18 +1095,18 @@ namespace Barotrauma.Items.Components { if (!character.IsOnPlayerTeam) { return; } if (character.Submarine != Submarine.MainSub) { return; } - string ammoType = container.ContainableItemIdentifiers.FirstOrDefault() ?? "ammobox"; + Identifier ammoType = container.ContainableItemIdentifiers.FirstOrNull() ?? "ammobox".ToIdentifier(); int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); if (remainingAmmo == 0) { - character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), - identifier: "outofammo", + character.Speak(TextManager.Get($"DialogOutOf{ammoType}", "DialogOutOfTurretAmmo").Value, + identifier: "outofammo".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } else if (remainingAmmo < 3) { - character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), - identifier: "outofammo", + character.Speak(TextManager.Get($"DialogLowOn{ammoType}").Value, + identifier: "outofammo".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } } @@ -1264,29 +1265,29 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { - if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName)) + if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName)) { - character.Speak(TextManager.Get("DialogNewTargetSpotted"), - identifier: "newtargetspotted", + character.Speak(TextManager.Get("DialogNewTargetSpotted").Value, + identifier: "newtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) + else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) { - character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), - identifier: "identifiedtargetspotted", + character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value, + identifier: "identifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } else { - character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), - identifier: "unidentifiedtargetspotted", + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, + identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } } - else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) + else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) { - character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), - identifier: "unidentifiedtargetspotted", + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, + identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } character.AddEncounter(closestEnemy); @@ -1295,8 +1296,8 @@ namespace Barotrauma.Items.Components } else if (closestEnemy == null && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogIceSpireSpotted"), - identifier: "icespirespotted", + character.Speak(TextManager.Get("DialogIceSpireSpotted").Value, + identifier: "icespirespotted".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } @@ -1339,8 +1340,8 @@ namespace Barotrauma.Items.Components if (!shoot) { return false; } if (character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogFireTurret"), - identifier: "fireturret", + character.Speak(TextManager.Get("DialogFireTurret").Value, + identifier: "fireturret".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } character.SetInput(InputType.Shoot, true, true); @@ -1349,6 +1350,14 @@ namespace Barotrauma.Items.Components return false; } + /// + /// Turret doesn't consume grid power, directly takes from the batteries on its grid instead. + /// + public override float GetCurrentPowerConsumption(Connection conn = null) + { + return 0; + } + private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) { if (targetBody == null) { return false; } @@ -1372,7 +1381,7 @@ namespace Barotrauma.Items.Components } if (ai != null) { - if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) + if (targetCharacter.Params.Group == ai.Config.Entity) { return false; } @@ -1458,7 +1467,7 @@ namespace Barotrauma.Items.Components for (int j = 0; j < item.linkedTo.Count; j++) { var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count]; - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (e is Item projectileContainer) { CheckProjectileContainer(projectileContainer, projectiles, out bool stopSearching); @@ -1602,7 +1611,7 @@ namespace Barotrauma.Items.Components private Vector2? loadedRotationLimits; private float? loadedBaseRotation; - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 93ada4ac4..784669f37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; +using System.Collections.Immutable; using Barotrauma.Abilities; namespace Barotrauma @@ -24,16 +25,16 @@ namespace Barotrauma class WearableSprite { - public string UnassignedSpritePath { get; private set; } + public ContentPath UnassignedSpritePath { get; private set; } public string SpritePath { get; private set; } - public XElement SourceElement { get; private set; } + public ContentXElement SourceElement { get; private set; } public WearableType Type { get; private set; } private Sprite _sprite; public Sprite Sprite { get { return _sprite; } - set + private set { if (value == _sprite) { return; } if (_sprite != null) @@ -85,29 +86,29 @@ namespace Barotrauma public int Variant { get; set; } - private Gender _gender; + private Character _picker; /// /// None = Any/Not Defined -> no effect. /// Changing the gender forces re-initialization, because the textures can be different for male and female characters. /// - public Gender Gender + public Character Picker { - get { return _gender; } + get { return _picker; } set { - if (value == _gender) { return; } - _gender = value; + if (value == _picker) { return; } + _picker = value; IsInitialized = false; - UnassignedSpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty)); - Init(_gender); + UnassignedSpritePath = ParseSpritePath(SourceElement); + Init(_picker); } } - public WearableSprite(XElement subElement, WearableType type) + public WearableSprite(ContentXElement subElement, WearableType type) { Type = type; SourceElement = subElement; - UnassignedSpritePath = subElement.GetAttributeString("texture", string.Empty); + UnassignedSpritePath = subElement.GetAttributeContentPath("texture") ?? ContentPath.Empty; Init(); switch (type) { @@ -131,34 +132,48 @@ namespace Barotrauma /// /// Note: this constructor cannot initialize automatically, because the gender is unknown at this point. We only know it when the item is equipped. /// - public WearableSprite(XElement subElement, Wearable wearable, int variant = 0) + public WearableSprite(ContentXElement subElement, Wearable wearable, int variant = 0) { Type = WearableType.Item; WearableComponent = wearable; Variant = Math.Max(variant, 0); - UnassignedSpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty)); + UnassignedSpritePath = ParseSpritePath(subElement); SourceElement = subElement; } - private string ParseSpritePath(string texturePath) => texturePath.Contains("/") ? texturePath : $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{texturePath}"; + private ContentPath ParseSpritePath(ContentXElement element) + { + if (element.DoesAttributeReferenceFileNameAlone("texture")) + { + string textureName = element.GetAttributeString("texture", ""); + return ContentPath.FromRaw( + element.ContentPackage, + $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{textureName}"); + } + else + { + return element.GetAttributeContentPath("texture") ?? ContentPath.Empty; + } + } public void ParsePath(bool parseSpritePath) { - string tempPath = UnassignedSpritePath; - if (_gender != Gender.None) + SpritePath = UnassignedSpritePath.Value; + if (_picker?.Info != null) { - tempPath = tempPath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male"); + SpritePath = _picker.Info.ReplaceVars(SpritePath); } - SpritePath = tempPath.Replace("[VARIANT]", Variant.ToString()); + SpritePath = SpritePath.Replace("[VARIANT]", Variant.ToString()); if (!File.Exists(SpritePath)) { // If the variant does not exist, parse the path so that it uses first variant. - SpritePath = tempPath.Replace("[VARIANT]", "1"); + SpritePath = SpritePath.Replace("[VARIANT]", "1"); } - if (!File.Exists(SpritePath) && _gender == Gender.None) + if (!File.Exists(SpritePath) && _picker?.Info == null) { - // If there's no sprite for Gender.None does not exist, try to use male sprite - SpritePath = tempPath.Replace("[GENDER]", "male"); + // If there's no character info is defined, try to use first tagset from CharacterInfoPrefab + var charInfoPrefab = CharacterPrefab.HumanPrefab.CharacterInfoPrefab; + SpritePath = charInfoPrefab.ReplaceVars(SpritePath, charInfoPrefab.Heads.First()); } if (parseSpritePath) { @@ -167,10 +182,11 @@ namespace Barotrauma } public bool IsInitialized { get; private set; } - public void Init(Gender gender = Gender.None) + public void Init(Character picker = null) { if (IsInitialized) { return; } - _gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None; + + _picker = picker; ParsePath(false); Sprite?.Remove(); Sprite = new Sprite(SourceElement, file: SpritePath); @@ -226,13 +242,13 @@ namespace Barotrauma.Items.Components { partial class Wearable : Pickable, IServerSerializable { - private readonly XElement[] wearableElements; + private readonly ContentXElement[] wearableElements; private readonly WearableSprite[] wearableSprites; private readonly LimbType[] limbType; private readonly Limb[] limb; private readonly List damageModifiers; - public readonly Dictionary SkillModifiers = new Dictionary(); + public readonly Dictionary SkillModifiers; public readonly Dictionary WearableStatValues = new Dictionary(); @@ -285,23 +301,24 @@ namespace Barotrauma.Items.Components } } - public Wearable(Item item, XElement element) : base(item, element) + public Wearable(Item item, ContentXElement element) : base(item, element) { this.item = item; damageModifiers = new List(); + SkillModifiers = new Dictionary(); int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); Variants = element.GetAttributeInt("variants", 0); - variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server); + variant = Rand.Range(1, Variants + 1, Rand.RandSync.ServerAndClient); wearableSprites = new WearableSprite[spriteCount]; - wearableElements = new XElement[spriteCount]; + wearableElements = new ContentXElement[spriteCount]; limbType = new LimbType[spriteCount]; limb = new Limb[spriteCount]; AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true); DisplayContainedStatus = element.GetAttributeBool("displaycontainedstatus", false); int i = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -318,7 +335,7 @@ namespace Barotrauma.Items.Components wearableSprites[i] = new WearableSprite(subElement, this, variant); wearableElements[i] = subElement; - foreach (XElement lightElement in subElement.Elements()) + foreach (var lightElement in subElement.Elements()) { if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } wearableSprites[i].LightComponent = new LightComponent(item, lightElement) @@ -334,7 +351,7 @@ namespace Barotrauma.Items.Components damageModifiers.Add(new DamageModifier(subElement, item.Name + ", Wearable")); break; case "skillmodifier": - string skillIdentifier = subElement.GetAttributeString("skillidentifier", string.Empty); + Identifier skillIdentifier = subElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); float skillValue = subElement.GetAttributeFloat("skillvalue", 0f); if (SkillModifiers.ContainsKey(skillIdentifier)) { @@ -382,12 +399,9 @@ namespace Barotrauma.Items.Components for (int i = 0; i < wearableSprites.Length; i++ ) { var wearableSprite = wearableSprites[i]; - if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker.Info?.Gender ?? Gender.None); } - if (picker.Info != null && picker.Info?.Gender != Gender.None && (wearableSprite.Gender != Gender.None)) - { - // If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated. - wearableSprite.Gender = picker.Info.Gender; - } + if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker); } + // If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated. + wearableSprite.Picker = picker; Limb equipLimb = character.AnimController.GetLimb(limbType[i]); if (equipLimb == null) { continue; } @@ -512,7 +526,7 @@ namespace Barotrauma.Items.Components } private int loadedVariant = -1; - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); loadedVariant = componentElement.GetAttributeInt("variant", -1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 69f16d500..4348caf46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -287,7 +287,7 @@ namespace Barotrauma if (DraggableIndicator == null) { - DraggableIndicator = GUI.Style.GetComponentStyle("GUIDragIndicator").GetDefaultSprite(); + DraggableIndicator = GUIStyle.GetComponentStyle("GUIDragIndicator").GetDefaultSprite(); slotHotkeySprite = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(258, 7, 120, 120), null, 0); @@ -479,15 +479,15 @@ namespace Barotrauma { if (i < 0 || i >= slots.Length) { - string thisItemStr = item?.prefab.Identifier ?? "null"; + string thisItemStr = item?.Prefab.Identifier.Value ?? "null"; string ownerStr = "null"; if (Owner is Item ownerItem) { - ownerStr = ownerItem.prefab.Identifier; + ownerStr = ownerItem.Prefab.Identifier.Value; } else if (Owner is Character ownerCharacter) { - ownerStr = ownerCharacter.SpeciesName; + ownerStr = ownerCharacter.SpeciesName.Value; } string errorMsg = $"Inventory.TryPutItem failed: index was out of range (item: {thisItemStr}, inventory: {ownerStr})."; GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); @@ -533,7 +533,7 @@ namespace Barotrauma else { #if CLIENT - if (visualSlots != null && createNetworkEvent) { visualSlots[i].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } + if (visualSlots != null && createNetworkEvent) { visualSlots[i].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } #endif return false; } @@ -766,11 +766,11 @@ namespace Barotrauma { for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); } + if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); } } for (int j = 0; j < otherInventory.capacity; j++) { - if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); } + if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); } } } #endif @@ -831,7 +831,7 @@ namespace Barotrauma { if (slots[j].Contains(existingItems.FirstOrDefault())) { - visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + visualSlots[j].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } } } @@ -890,15 +890,15 @@ namespace Barotrauma return list; } - public Item FindItemByTag(string tag, bool recursive = false) + public Item FindItemByTag(Identifier tag, bool recursive = false) { - if (tag == null) { return null; } + if (tag.IsEmpty) { return null; } return FindItem(i => i.HasTag(tag), recursive); } - public Item FindItemByIdentifier(string identifier, bool recursive = false) + public Item FindItemByIdentifier(Identifier identifier, bool recursive = false) { - if (identifier == null) { return null; } + if (identifier.IsEmpty) { return null; } return FindItem(i => i.Prefab.Identifier == identifier, recursive); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index cfe23ac81..e892953f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; +using System.Collections.Immutable; using Barotrauma.Abilities; #if CLIENT @@ -22,11 +23,11 @@ namespace Barotrauma partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable { public static List ItemList = new List(); - public ItemPrefab Prefab => prefab as ItemPrefab; + public new ItemPrefab Prefab => base.Prefab as ItemPrefab; public static bool ShowLinks = true; - - private readonly HashSet tags; + + private readonly HashSet tags; private bool isWire, isLogic; @@ -119,7 +120,7 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType; private readonly Dictionary> statusEffectLists; - public Dictionary SerializableProperties { get; protected set; } + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; bool HasInGameEditableProperties @@ -186,17 +187,17 @@ namespace Barotrauma public override string Name { - get { return prefab.Name; } + get { return base.Prefab.Name.Value; } } private string description; public string Description { - get { return description ?? prefab.Description; } + get { return description ?? base.Prefab.Description.Value; } set { description = value; } } - [Editable, Serialize(false, true, alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool NonInteractable { get; @@ -206,21 +207,21 @@ namespace Barotrauma /// /// Use to also check /// - [Editable, Serialize(false, true, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)] public bool NonPlayerTeamInteractable { get; set; } - [ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, true, alwaysUseInstanceValues: true)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool AllowSwapping { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool PurchasedNewSwap { get; @@ -262,7 +263,7 @@ namespace Barotrauma private float rotationRad; - [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, true)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)] public float Rotation { get @@ -331,7 +332,7 @@ namespace Barotrauma if (scale == value) { return; } scale = MathHelper.Clamp(value, 0.01f, 10.0f); - float relativeScale = scale / prefab.Scale; + float relativeScale = scale / base.Prefab.Scale; if (!ResizeHorizontal || !ResizeVertical) { @@ -357,21 +358,21 @@ namespace Barotrauma } = float.PositiveInfinity; protected Color spriteColor; - [Editable, Serialize("1.0,1.0,1.0,1.0", true)] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)] public Color SpriteColor { get { return spriteColor; } set { spriteColor = value; } } - [Serialize("1.0,1.0,1.0,1.0", true), Editable] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable] public Color InventoryIconColor { get; protected set; } - - [Editable, Serialize("1.0,1.0,1.0,1.0", true, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] + + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] public Color ContainerColor { get; @@ -381,26 +382,26 @@ namespace Barotrauma /// /// Can be used by status effects or conditionals to check what item this item is contained inside /// - public string ContainerIdentifier + public Identifier ContainerIdentifier { get { return - Container?.prefab.Identifier ?? - ParentInventory?.Owner?.ToString() ?? - ""; + Container?.Prefab.Identifier ?? + ParentInventory?.Owner?.ToIdentifier() ?? + Identifier.Empty; } } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] /// /// Can be used to modify the AITarget's label using status effects /// public string SonarLabel { - get { return AiTarget?.SonarLabel ?? ""; } + get { return AiTarget?.SonarLabel?.Value ?? ""; } set { if (AiTarget != null) @@ -421,7 +422,7 @@ namespace Barotrauma } } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sound range /// @@ -431,7 +432,7 @@ namespace Barotrauma set { if (aiTarget != null) { aiTarget.SoundRange = Math.Max(0.0f, value); } } } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sound range /// @@ -444,13 +445,13 @@ namespace Barotrauma /// /// Should the item's Use method be called with the "Use" or with the "Shoot" key? /// - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool IsShootable { get; set; } /// /// If true, the user has to hold the "aim" key before use is registered. False by default. /// - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool RequireAimToUse { get; set; @@ -459,7 +460,7 @@ namespace Barotrauma /// /// If true, the user has to hold the "aim" key before secondary use is registered. True by default. /// - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool RequireAimToSecondaryUse { get; set; @@ -475,8 +476,8 @@ namespace Barotrauma public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition); private float offsetOnSelectedMultiplier = 1.0f; - - [Serialize(1.0f, false)] + + [Serialize(1.0f, IsPropertySaveable.No)] public float OffsetOnSelectedMultiplier { get => offsetOnSelectedMultiplier; @@ -485,7 +486,7 @@ namespace Barotrauma private float healthMultiplier = 1.0f; - [Serialize(1.0f, true, "Multiply the maximum condition by this value")] + [Serialize(1.0f, IsPropertySaveable.Yes, "Multiply the maximum condition by this value")] public float HealthMultiplier { get => healthMultiplier; @@ -499,7 +500,7 @@ namespace Barotrauma private float maxRepairConditionMultiplier = 1.0f; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float MaxRepairConditionMultiplier { get => maxRepairConditionMultiplier; @@ -508,7 +509,7 @@ namespace Barotrauma //the default value should be Prefab.Health, but because we can't use it in the attribute, //we'll just use NaN (which does nothing) and set the default value in the constructor/load - [Serialize(float.NaN, false), Editable] + [Serialize(float.NaN, IsPropertySaveable.No), Editable] public float Condition { get { return condition; } @@ -517,7 +518,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (!MathUtils.IsValid(value)) { return; } if (Indestructible) { return; } - if (InvulnerableToDamage && value <= condition) { return;} + if (InvulnerableToDamage && value <= condition) { return; } float prev = condition; bool wasInFullCondition = IsFullCondition; @@ -525,6 +526,8 @@ namespace Barotrauma condition = MathHelper.Clamp(value, 0.0f, MaxCondition); if (condition == 0.0f && prev > 0.0f) { + //Flag connections to be updated as device is broken + flagChangedConnections(connections); #if CLIENT foreach (ItemComponent ic in components) { @@ -534,6 +537,11 @@ namespace Barotrauma #endif ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); } + else if (condition > 0.0f && prev <= 0.0f) + { + //Flag connections to be updated as device is now working again + flagChangedConnections(connections); + } SetActiveSprite(); @@ -559,6 +567,22 @@ namespace Barotrauma LastConditionChange = condition - prev; ConditionLastUpdated = Timing.TotalTime; + + static void flagChangedConnections(Dictionary connections) + { + if (connections == null) { return; } + foreach (Connection c in connections.Values) + { + if (c.IsPower && c.Grid != null) + { + Powered.ChangedConnections.Add(c); + foreach (Connection conn in c.Recipients) + { + Powered.ChangedConnections.Add(conn); + } + } + } + } } } @@ -590,7 +614,7 @@ namespace Barotrauma set; } - [Editable, Serialize(false, isSaveable: true, "When enabled will prevent the item from taking damage from all sources")] + [Editable, Serialize(false, isSaveable: IsPropertySaveable.Yes, "When enabled will prevent the item from taking damage from all sources")] public bool InvulnerableToDamage { get; set; } public bool StolenDuringRound; @@ -609,7 +633,7 @@ namespace Barotrauma } } - [Serialize(true, true, alwaysUseInstanceValues: true)] + [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool AllowStealing { get; @@ -617,7 +641,7 @@ namespace Barotrauma } private string originalOutpost; - [Serialize("", true, alwaysUseInstanceValues: true)] + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public string OriginalOutpost { get { return originalOutpost; } @@ -631,7 +655,7 @@ namespace Barotrauma } } - [Editable, Serialize("", true)] + [Editable, Serialize("", IsPropertySaveable.Yes)] public string Tags { get { return string.Join(",", tags); } @@ -639,7 +663,7 @@ namespace Barotrauma { tags.Clear(); // Always add prefab tags - prefab.Tags.ForEach(t => tags.Add(t)); + base.Prefab.Tags.ForEach(t => tags.Add(t)); if (!string.IsNullOrWhiteSpace(value)) { string[] splitTags = value.Split(','); @@ -647,7 +671,7 @@ namespace Barotrauma { string[] splitTag = tag.Trim().Split(':'); splitTag[0] = splitTag[0].ToLowerInvariant(); - tags.Add(string.Join(":", splitTag)); + tags.Add(string.Join(":", splitTag).ToIdentifier()); } } } @@ -705,10 +729,7 @@ namespace Barotrauma private set; } = new List(20); - public string ConfigFile - { - get { return Prefab.FilePath; } - } + public ContentPath ConfigFilePath => Prefab.ContentFile.Path; //which type of inventory slots (head, torso, any, etc) the item can be placed in private readonly HashSet allowedSlots = new HashSet(); @@ -743,7 +764,7 @@ namespace Barotrauma get { return ownInventory; } } - [Editable, Serialize(false, true, description: + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " + "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")] public bool DisplaySideBySideWhenLinked { get; set; } @@ -806,13 +827,13 @@ namespace Barotrauma public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } - public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID) + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( - (int)(position.X - itemPrefab.sprite.size.X / 2 * itemPrefab.Scale), - (int)(position.Y + itemPrefab.sprite.size.Y / 2 * itemPrefab.Scale), - (int)(itemPrefab.sprite.size.X * itemPrefab.Scale), - (int)(itemPrefab.sprite.size.Y * itemPrefab.Scale)), - itemPrefab, submarine, id: id) + (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), + (int)(position.Y + itemPrefab.Sprite.size.Y / 2 * itemPrefab.Scale), + (int)(itemPrefab.Sprite.size.X * itemPrefab.Scale), + (int)(itemPrefab.Sprite.size.Y * itemPrefab.Scale)), + itemPrefab, submarine, callOnItemLoaded, id: id) { } @@ -824,11 +845,11 @@ namespace Barotrauma public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true, ushort id = Entity.NullEntityID) : base(itemPrefab, submarine, id) { - spriteColor = prefab.SpriteColor; + spriteColor = base.Prefab.SpriteColor; components = new List(); drawableComponents = new List(); hasComponentsToDraw = false; - tags = new HashSet(); + tags = new HashSet(); repairables = new List(); defaultRect = newRect; @@ -841,7 +862,7 @@ namespace Barotrauma allPropertyObjects.Add(this); - XElement element = itemPrefab.ConfigElement; + ContentXElement element = itemPrefab.ConfigElement; if (element == null) return; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -850,7 +871,7 @@ namespace Barotrauma SetActiveSprite(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -924,7 +945,7 @@ namespace Barotrauma aiTarget = new AITarget(this, subElement); break; default: - ItemComponent ic = ItemComponent.Load(subElement, this, itemPrefab.FilePath); + ItemComponent ic = ItemComponent.Load(subElement, this); if (ic == null) break; AddComponent(ic); @@ -1041,7 +1062,7 @@ namespace Barotrauma { defaultRect = defaultRect }; - foreach (KeyValuePair property in SerializableProperties) + foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); @@ -1058,7 +1079,7 @@ namespace Barotrauma for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - foreach (KeyValuePair property in components[i].SerializableProperties) + foreach (KeyValuePair property in components[i].SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i])); @@ -1269,9 +1290,9 @@ namespace Barotrauma if (!Prefab.AllowDroppingOnSwap || otherItem == null) { return false; } if (Prefab.AllowDroppingOnSwapWith.Any()) { - foreach (string tagOrIdentifier in Prefab.AllowDroppingOnSwapWith) + foreach (Identifier tagOrIdentifier in Prefab.AllowDroppingOnSwapWith) { - if (otherItem.prefab.Identifier.Equals(tagOrIdentifier, StringComparison.OrdinalIgnoreCase)) { return true; } + if (otherItem.Prefab.Identifier == tagOrIdentifier) { return true; } if (otherItem.HasTag(tagOrIdentifier)) { return true; } } return false; @@ -1386,31 +1407,22 @@ namespace Barotrauma public Item GetRootContainer() { if (Container == null) { return null; } - Item rootContainer = Container; while (rootContainer.Container != null) { rootContainer = rootContainer.Container; } - return rootContainer; } - /// - /// Should this item or any of its containers be ignored by the AI? - /// - public bool IsThisOrAnyContainerIgnoredByAI(Character character) + public bool HasAccess(Character character) { - if (IgnoreByAI(character)) { return true; } - if (Container == null) { return false; } - if (Container.IgnoreByAI(character)) { return true; } - var container = Container; - while (container.Container != null) - { - container = container.Container; - if (container.IgnoreByAI(character)) { return true; } - } - return false; + if (character.IsBot && IgnoreByAI(character)) { return false; } + if (!IsInteractable(character)) { return false; } + var itemContainer = GetComponent(); + if (itemContainer != null && !itemContainer.HasAccess(character)) { return false; } + if (Container != null && !Container.HasAccess(character)) { return false; } + return true; } public bool IsOwnedBy(Entity entity) => FindParentInventory(i => i.Owner == entity) != null; @@ -1449,33 +1461,49 @@ namespace Barotrauma } public void AddTag(string tag) + { + AddTag(tag.ToIdentifier()); + } + + public void AddTag(Identifier tag) { if (tags.Contains(tag)) { return; } tags.Add(tag); } + public bool HasTag(string tag) + { + return HasTag(tag.ToIdentifier()); + } + + public bool HasTag(Identifier tag) { if (tag == null) { return true; } - return tags.Contains(tag) || prefab.Tags.Contains(tag); + return tags.Contains(tag) || base.Prefab.Tags.Contains(tag); } public void ReplaceTag(string tag, string newTag) + { + ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier()); + } + + public void ReplaceTag(Identifier tag, Identifier newTag) { if (!tags.Contains(tag)) { return; } tags.Remove(tag); tags.Add(newTag); } - public IEnumerable GetTags() + public IEnumerable GetTags() { return tags; } - public bool HasTag(IEnumerable allowedTags) + public bool HasTag(IEnumerable allowedTags) { if (allowedTags == null) return true; - foreach (string tag in allowedTags) + foreach (Identifier tag in allowedTags) { if (tags.Contains(tag)) return true; } @@ -1528,7 +1556,7 @@ namespace Barotrauma foreach (Item containedItem in ContainedItems) { if (effect.TargetIdentifiers != null && - !effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) && + !effect.TargetIdentifiers.Contains(((MapEntity)containedItem).Prefab.Identifier) && !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id))) { continue; @@ -1731,7 +1759,7 @@ namespace Barotrauma UpdateTransform(); if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) { - Spawner?.AddToRemoveQueue(this); + Spawner?.AddItemToRemoveQueue(this); return; } } @@ -1860,7 +1888,7 @@ namespace Barotrauma //apply simple angular drag body.ApplyTorque(body.AngularVelocity * volume * -0.05f); - } + } private bool OnCollision(Fixture f1, Fixture f2, Contact contact) @@ -1871,14 +1899,9 @@ namespace Barotrauma if (projectile != null) { //ignore character colliders (a projectile only hits limbs) - if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) - { - return false; - } - if (projectile.IgnoredBodies != null) - { - if (projectile.IgnoredBodies.Contains(f2.Body)) { return false; } - } + if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; } + if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; } + if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; } } contact.GetWorldManifold(out Vector2 normal, out _); @@ -2027,17 +2050,17 @@ namespace Barotrauma return connectedComponents; } - public static readonly (string input, string output)[] connectionPairs = new (string input, string output)[] + public static readonly ImmutableArray<(Identifier Input, Identifier Output)> connectionPairs = new (Identifier, Identifier)[] { - ("power_in", "power_out"), - ("signal_in1", "signal_out1"), - ("signal_in2", "signal_out2"), - ("signal_in3", "signal_out3"), - ("signal_in4", "signal_out4"), - ("signal_in", "signal_out"), - ("signal_in1", "signal_out"), - ("signal_in2", "signal_out") - }; + ("power_in".ToIdentifier(), "power_out".ToIdentifier()), + ("signal_in1".ToIdentifier(), "signal_out1".ToIdentifier()), + ("signal_in2".ToIdentifier(), "signal_out2".ToIdentifier()), + ("signal_in3".ToIdentifier(), "signal_out3".ToIdentifier()), + ("signal_in4".ToIdentifier(), "signal_out4".ToIdentifier()), + ("signal_in".ToIdentifier(), "signal_out".ToIdentifier()), + ("signal_in1".ToIdentifier(), "signal_out".ToIdentifier()), + ("signal_in2".ToIdentifier(), "signal_out".ToIdentifier()) + }.ToImmutableArray(); private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays) where T : ItemComponent { @@ -2078,34 +2101,30 @@ namespace Barotrauma if (relay != null && !relay.IsOn) { return; } } - foreach ((string input, string output) in connectionPairs) + foreach ((Identifier input, Identifier output) in connectionPairs) { - if (input == c.Name) + void searchFromAToB(Identifier connectionEndA, Identifier connectionEndB) { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == output); - if (pairedConnection != null) + if (connectionEndA == c.Name) { - if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); - } - } - else if (output == c.Name) - { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == input); - if (pairedConnection != null) - { - if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); + var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionEndB); + if (pairedConnection != null) + { + if (alreadySearched.Contains(pairedConnection)) { return; } + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); + } } } + searchFromAToB(input, output); + searchFromAToB(output, input); } } - public Controller FindController(string[] tags = null) + public Controller FindController(ImmutableArray? tags = null) { //try finding the controller with the simpler non-recursive method first var controllers = GetConnectedComponents(); - bool needsTag = tags != null && tags.Length > 0; + bool needsTag = tags != null && tags.Value.Length > 0; if (controllers.None() || (needsTag && controllers.None(c => c.Item.HasTag(tags)))) { controllers = GetConnectedComponents(recursive: true); @@ -2119,7 +2138,7 @@ namespace Barotrauma controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault(); } - public bool TryFindController(out Controller controller, string[] tags = null) + public bool TryFindController(out Controller controller, ImmutableArray? tags = null) { controller = FindController(tags: tags); return controller != null; @@ -2291,11 +2310,12 @@ namespace Barotrauma //to prevent accidentally selecting items when clicking UI elements if (user == Character.Controlled && GUI.MouseOn != null) { - if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0) + if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.PickKey].MouseButton == 0) { pickHit = false; } - if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0) + + if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.SelectKey].MouseButton == 0) { selectHit = false; } @@ -2308,7 +2328,7 @@ namespace Barotrauma //LMB is used to manipulate wires, so using E to select connection panels is much easier if (Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode) { - pickHit = selectHit = GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None ? + pickHit = selectHit = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None ? user.IsKeyHit(InputType.Use) : user.IsKeyHit(InputType.Select); } @@ -2356,8 +2376,9 @@ namespace Barotrauma { if (requiredSkill != null) { - GUI.AddMessage(TextManager.GetWithVariables("InsufficientSkills", new string[2] { "[requiredskill]", "[requiredlevel]" }, - new string[2] { TextManager.Get("SkillName." + requiredSkill.Identifier), ((int)(requiredSkill.Level * skillMultiplier)).ToString() }, new bool[2] { true, false }), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariables("InsufficientSkills", + ("[requiredskill]", TextManager.Get("SkillName." + requiredSkill.Identifier), FormatCapitals.Yes), + ("[requiredlevel]", ((int)(requiredSkill.Level * skillMultiplier)).ToString(), FormatCapitals.No)), GUIStyle.Red); } } #endif @@ -2423,7 +2444,7 @@ namespace Barotrauma if (remove) { - Spawner.AddToRemoveQueue(this); + Spawner.AddItemToRemoveQueue(this); } } @@ -2456,7 +2477,7 @@ namespace Barotrauma if (remove) { - Spawner.AddToRemoveQueue(this); + Spawner.AddItemToRemoveQueue(this); } } @@ -2520,7 +2541,7 @@ namespace Barotrauma } - if (remove) { Spawner?.AddToRemoveQueue(this); } + if (remove) { Spawner?.AddItemToRemoveQueue(this); } } public bool Combine(Item item, Character user) @@ -2644,6 +2665,10 @@ namespace Barotrauma { msg.Write(stringVal); } + else if (value is Identifier idValue) + { + msg.Write(idValue); + } else if (value is float floatVal) { msg.Write(floatVal); @@ -2770,6 +2795,12 @@ namespace Barotrauma property.TrySetValue(parentObject, val); } } + else if (type == typeof(Identifier)) + { + Identifier val = msg.ReadIdentifier(); + logValue = val.Value; + if (allowEditing) { property.TrySetValue(parentObject, val); } + } else if (type == typeof(float)) { float val = msg.ReadSingle(); @@ -2887,7 +2918,7 @@ namespace Barotrauma partial void UpdateNetPosition(float deltaTime); - public static Item Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Item Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { return Load(element, submarine, createNetworkEvent: false, idRemap: idRemap); } @@ -2899,12 +2930,12 @@ namespace Barotrauma /// The submarine to spawn the item in (can be null) /// Should an EntitySpawner event be created to notify clients about the item being created. /// - public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) + public static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) { string name = element.Attribute("name").Value; - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); - if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(identifier)) + if (string.IsNullOrWhiteSpace(name) && identifier.IsEmpty) { string errorMessage = "Failed to load an item (both name and identifier were null):\n"+element.ToString(); DebugConsole.ThrowError(errorMessage); @@ -2912,15 +2943,15 @@ namespace Barotrauma return null; } - string pendingSwap = element.GetAttributeString("pendingswap", ""); + Identifier pendingSwap = element.GetAttributeIdentifier("pendingswap", Identifier.Empty); ItemPrefab appliedSwap = null; ItemPrefab oldPrefab = null; - if (!string.IsNullOrEmpty(pendingSwap) && Level.Loaded?.Type != LevelData.LevelType.Outpost) + if (!pendingSwap.IsEmpty && Level.Loaded?.Type != LevelData.LevelType.Outpost) { oldPrefab = ItemPrefab.Find(name, identifier); appliedSwap = ItemPrefab.Find(string.Empty, pendingSwap); identifier = pendingSwap; - pendingSwap = null; + pendingSwap = Identifier.Empty; } ItemPrefab prefab = ItemPrefab.Find(name, identifier); @@ -2930,8 +2961,8 @@ namespace Barotrauma Vector2 centerPos = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2); if (appliedSwap != null) { - rect.Width = (int)(prefab.sprite.size.X * prefab.Scale); - rect.Height = (int)(prefab.sprite.size.Y * prefab.Scale); + rect.Width = (int)(prefab.Sprite.size.X * prefab.Scale); + rect.Height = (int)(prefab.Sprite.size.Y * prefab.Scale); } else if (rect.Width == 0 && rect.Height == 0) { @@ -2943,7 +2974,7 @@ namespace Barotrauma { Submarine = submarine, linkedToID = new List(), - PendingItemSwap = string.IsNullOrEmpty(pendingSwap) ? null : MapEntityPrefab.Find(pendingSwap) as ItemPrefab + PendingItemSwap = pendingSwap.IsEmpty ? null : MapEntityPrefab.Find(pendingSwap.Value) as ItemPrefab }; #if SERVER @@ -2955,11 +2986,11 @@ namespace Barotrauma foreach (XAttribute attribute in (appliedSwap?.ConfigElement ?? element).Attributes()) { - if (!item.SerializableProperties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) { continue; } + if (!item.SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; } bool shouldBeLoaded = false; foreach (var propertyAttribute in property.Attributes.OfType()) { - if (propertyAttribute.isSaveable) + if (propertyAttribute.IsSaveable == IsPropertySaveable.Yes) { shouldBeLoaded = true; break; @@ -2975,19 +3006,18 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType().Any() && (submarine == null || !submarine.Loading)) { - switch (property.Name) + if (property.Name == "Tags" || + property.Name == "Condition" || + property.Name == "Description") { - case "Tags": - case "Condition": - case "Description": - //these can be ignored, they're always written in the spawn data - break; - default: - if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) - { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, property }); - } - break; + //these can be ignored, they're always written in the spawn data + } + else + { + if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) + { + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, property }); + } } } } @@ -2999,15 +3029,15 @@ namespace Barotrauma //if we're overriding a non-overridden item in a sub/assembly xml or vice versa, //use the values from the prefab instead of loading them from the sub/assembly xml - bool usePrefabValues = thisIsOverride != prefab.IsOverride || appliedSwap != null; + bool usePrefabValues = thisIsOverride != ItemPrefab.Prefabs.IsOverride(prefab) || appliedSwap != null; List unloadedComponents = new List(item.components); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "upgrade": { - var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty); + var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier); int level = subElement.GetAttributeInt("level", 1); if (upgradePrefab != null) @@ -3039,8 +3069,8 @@ namespace Barotrauma item.Upgrades.ForEach(upgrade => upgrade.ApplyUpgrade()); - var availableSwapIds = element.GetAttributeStringArray("availableswaps", new string[0]); - foreach (string swapId in availableSwapIds) + var availableSwapIds = element.GetAttributeIdentifierArray("availableswaps", Array.Empty()); + foreach (Identifier swapId in availableSwapIds) { ItemPrefab swapPrefab = ItemPrefab.Find(string.Empty, swapId); if (swapPrefab != null) @@ -3140,7 +3170,7 @@ namespace Barotrauma if (Rotation != 0f) { element.Add(new XAttribute("rotation", Rotation)); } - if (Prefab.IsOverride) { element.Add(new XAttribute("isoverride", "true")); } + if (ItemPrefab.Prefabs.IsOverride(Prefab)) { element.Add(new XAttribute("isoverride", "true")); } if (FlippedX) { element.Add(new XAttribute("flippedx", true)); } if (FlippedY) { element.Add(new XAttribute("flippedy", true)); } @@ -3332,7 +3362,7 @@ namespace Barotrauma List list = new List(ItemList); foreach (Item item in list) { - if (item.prefab == prefab) + if (((MapEntity)item).Prefab == prefab) { item.Remove(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 40cfa040f..23137fab7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -4,15 +4,16 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.Linq; +using Barotrauma.Extensions; +using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma { - struct DeconstructItem + readonly struct DeconstructItem { - public readonly string ItemIdentifier; + public readonly Identifier ItemIdentifier; //minCondition does <= check, meaning that below or equal to min condition will be skipped. public readonly float MinCondition; //maxCondition does > check, meaning that above this max the deconstruct item will be skipped. @@ -21,6 +22,7 @@ namespace Barotrauma public readonly float OutConditionMin, OutConditionMax; //should the condition of the deconstructed item be copied to the output items public readonly bool CopyCondition; + //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this public readonly string[] RequiredDeconstructor; //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this @@ -30,11 +32,11 @@ namespace Barotrauma public readonly string InfoText; public readonly string InfoTextOnOtherItemMissing; - public float Commonness { get; } + public readonly float Commonness; - public DeconstructItem(XElement element, string parentDebugName) + public DeconstructItem(XElement element, Identifier parentDebugName) { - ItemIdentifier = element.GetAttributeString("identifier", "notfound"); + ItemIdentifier = element.GetAttributeIdentifier("identifier", ""); MinCondition = element.GetAttributeFloat("mincondition", -0.1f); MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f); OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f)); @@ -56,85 +58,113 @@ namespace Barotrauma class FabricationRecipe { - public class RequiredItem + public abstract class RequiredItem { - public readonly List ItemPrefabs; - public int Amount; + public abstract IEnumerable ItemPrefabs { get; } + public abstract UInt32 UintIdentifier { get; } + + public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition) + { + Amount = amount; + MinCondition = minCondition; + MaxCondition = maxCondition; + UseCondition = useCondition; + } + public readonly int Amount; public readonly float MinCondition; public readonly float MaxCondition; public readonly bool UseCondition; + } - public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) - { - ItemPrefabs = new List() { itemPrefab }; - Amount = amount; - MinCondition = minCondition; - MaxCondition = maxCondition; - UseCondition = useCondition; - } + public class RequiredItemByIdentifier : RequiredItem + { + public readonly Identifier ItemPrefabIdentifier; + public ItemPrefab ItemPrefab => ItemPrefab.Prefabs[ItemPrefabIdentifier]; + public override UInt32 UintIdentifier { get; } - public RequiredItem(IEnumerable itemPrefabs, int amount, float minCondition, float maxCondition, bool useCondition) + public override IEnumerable ItemPrefabs => ItemPrefab.ToEnumerable(); + + public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) { - ItemPrefabs = new List(itemPrefabs); - Amount = amount; - MinCondition = minCondition; - MaxCondition = maxCondition; - UseCondition = useCondition; + ItemPrefabIdentifier = itemPrefab; + using MD5 md5 = MD5.Create(); + UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5); } } - public readonly ItemPrefab TargetItem; - public readonly string DisplayName; - public readonly List RequiredItems; - public readonly string[] SuitableFabricatorIdentifiers; + public class RequiredItemByTag : RequiredItem + { + public readonly Identifier Tag; + public override UInt32 UintIdentifier { get; } + + public override IEnumerable ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)); + + public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) + { + Tag = tag; + using MD5 md5 = MD5.Create(); + UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5); + } + } + + public readonly Identifier TargetItemPrefabIdentifier; + public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier]; + + private Lazy displayName; + public LocalizedString DisplayName + => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : ""; + public readonly ImmutableArray RequiredItems; + public readonly ImmutableArray SuitableFabricatorIdentifiers; public readonly float RequiredTime; public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 - public readonly List RequiredSkills; + public readonly ImmutableArray RequiredSkills; + public readonly uint RecipeHash; + public readonly int Amount; - public int Amount { get; } - - public FabricationRecipe(XElement element, ItemPrefab itemPrefab) + public FabricationRecipe(XElement element, Identifier itemPrefab) { - TargetItem = itemPrefab; - string displayName = element.GetAttributeString("displayname", ""); - DisplayName = string.IsNullOrEmpty(displayName) ? itemPrefab.Name : TextManager.GetWithVariable($"DisplayName.{displayName}", "[itemname]", itemPrefab.Name); + TargetItemPrefabIdentifier = itemPrefab; + var displayNameIdentifier = element.GetAttributeIdentifier("displayname", ""); + displayName = new Lazy(() => displayNameIdentifier.IsEmpty + ? TargetItem.Name + : TextManager.GetWithVariable($"DisplayName.{displayNameIdentifier}", "[itemname]", TargetItem.Name)); - SuitableFabricatorIdentifiers = element.GetAttributeStringArray("suitablefabricators", new string[0]); + SuitableFabricatorIdentifiers = element.GetAttributeIdentifierArray("suitablefabricators", Array.Empty()).ToImmutableArray(); - RequiredSkills = new List(); + var requiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); if (OutCondition > 1.0f) { - DebugConsole.AddWarning($"Error in \"{itemPrefab.Name}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); + DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); } - RequiredItems = new List(); + var requiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "requiredskill": if (subElement.Attribute("name") != null) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Use skill identifiers instead of names."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names."); continue; } - RequiredSkills.Add(new Skill( - subElement.GetAttributeString("identifier", ""), + requiredSkills.Add(new Skill( + subElement.GetAttributeIdentifier("identifier", ""), subElement.GetAttributeInt("level", 0))); break; case "item": case "requireditem": - string requiredItemIdentifier = subElement.GetAttributeString("identifier", ""); - string requiredItemTag = subElement.GetAttributeString("tag", ""); - if (string.IsNullOrWhiteSpace(requiredItemIdentifier) && string.IsNullOrEmpty(requiredItemTag)) + Identifier requiredItemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + Identifier requiredItemTag = subElement.GetAttributeIdentifier("tag", Identifier.Empty); + if (requiredItemIdentifier == Identifier.Empty && requiredItemTag == Identifier.Empty) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! One of the required items has no identifier or tag."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag."); continue; } @@ -144,69 +174,76 @@ namespace Barotrauma bool useCondition = subElement.GetAttributeBool("usecondition", true); int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1)); - if (!string.IsNullOrEmpty(requiredItemIdentifier)) + if (requiredItemIdentifier != Identifier.Empty) { - if (!(MapEntityPrefab.Find(null, requiredItemIdentifier.Trim()) is ItemPrefab requiredItem)) + var existing = requiredItems.FindIndex(r => + r is RequiredItemByIdentifier ri && + ri.ItemPrefabIdentifier == requiredItemIdentifier && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && + MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); + if (existing >= 0) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Required item \"" + requiredItemIdentifier + "\" not found."); - continue; - } - - var existing = RequiredItems.Find(r => - r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && - MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); - if (existing == null) - { - RequiredItems.Add(new RequiredItem(requiredItem, amount, minCondition, maxCondition, useCondition)); - } - else - { - existing.Amount += amount; + amount += requiredItems[existing].Amount; + requiredItems.RemoveAt(existing); } + requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition)); } else { - var matchingItems = ItemPrefab.Prefabs.Where(ip => ip.Tags.Any(t => t.Equals(requiredItemTag, StringComparison.OrdinalIgnoreCase))); - if (!matchingItems.Any()) + var existing = requiredItems.FindIndex(r => + r is RequiredItemByTag rt && + rt.Tag == requiredItemTag && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && + MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); + if (existing >= 0) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Could not find any items with the tag \"" + requiredItemTag + "\"."); - continue; - } - - var existing = RequiredItems.Find(r => - r.ItemPrefabs.SequenceEqual(matchingItems) && - MathUtils.NearlyEqual(r.MinCondition, minCondition) && - MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); - if (existing == null) - { - RequiredItems.Add(new RequiredItem(matchingItems, amount, minCondition, maxCondition, useCondition)); - } - else - { - existing.Amount += amount; + amount += requiredItems[existing].Amount; + requiredItems.RemoveAt(existing); } + requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition)); } break; } } + + this.RequiredSkills = requiredSkills.ToImmutableArray(); + this.RequiredItems = requiredItems.ToImmutableArray(); + + RecipeHash = GenerateHash(); + } + + private uint GenerateHash() + { + using var md5 = MD5.Create(); + uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5); + + var requiredItems = string.Join(':', RequiredItems + .Select(i => i.UintIdentifier) + .Select(i => string.Join(',', i))); + + var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}")); + + uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{requiredItems}|{requiredSkills}", md5); + if (retVal == 0) { retVal = 1; } + return retVal; } } class PreferredContainer { - public readonly HashSet Primary = new HashSet(); - public readonly HashSet Secondary = new HashSet(); + public readonly ImmutableHashSet Primary; + public readonly ImmutableHashSet Secondary; - public float SpawnProbability { get; private set; } - public float MaxCondition { get; private set; } - public float MinCondition { get; private set; } - public int MinAmount { get; private set; } - public int MaxAmount { get; private set; } + public readonly float SpawnProbability; + public readonly float MaxCondition; + public readonly float MinCondition; + public readonly int MinAmount; + public readonly int MaxAmount; public PreferredContainer(XElement element) { - Primary = XMLExtensions.GetAttributeStringArray(element, "primary", new string[0]).ToHashSet(); - Secondary = XMLExtensions.GetAttributeStringArray(element, "secondary", new string[0]).ToHashSet(); + Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty()).ToImmutableHashSet(); + Secondary = XMLExtensions.GetAttributeIdentifierArray(element, "secondary", Array.Empty()).ToImmutableHashSet(); SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); MinAmount = element.GetAttributeInt("minamount", 0); MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0)); @@ -236,7 +273,7 @@ namespace Barotrauma public readonly bool CanBeBought; - public readonly string ReplacementOnUninstall; + public readonly Identifier ReplacementOnUninstall; public string SpawnWithId; @@ -244,7 +281,7 @@ namespace Barotrauma public readonly Vector2 SwapOrigin; - public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>(); + public List<(Identifier requiredTag, Identifier swapTo)> ConnectedItemsToSwap = new List<(Identifier requiredTag, Identifier swapTo)>(); public readonly Sprite SchematicSprite; @@ -254,16 +291,16 @@ namespace Barotrauma return location?.GetAdjustedMechanicalCost(price) ?? price; } - public SwappableItem(XElement element) + public SwappableItem(ContentXElement element) { BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0); SwapIdentifier = element.GetAttributeString("swapidentifier", string.Empty); CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0); - ReplacementOnUninstall = element.GetAttributeString("replacementonuninstall", ""); + ReplacementOnUninstall = element.GetAttributeIdentifier("replacementonuninstall", ""); SwapOrigin = element.GetAttributeVector2("origin", Vector2.One); SpawnWithId = element.GetAttributeString("spawnwithid", string.Empty); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -272,319 +309,53 @@ namespace Barotrauma break; case "swapconnecteditem": ConnectedItemsToSwap.Add( - (subElement.GetAttributeString("tag", ""), - subElement.GetAttributeString("swapto", ""))); + (subElement.GetAttributeIdentifier("tag", ""), + subElement.GetAttributeIdentifier("swapto", ""))); break; } } } } - partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier + partial class ItemPrefab : MapEntityPrefab, IImplementsVariants { - private readonly string name; - public override string Name => name; - public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - Item.RemoveByPrefab(this); - } - //default size - protected Vector2 size; + public Vector2 Size { get; private set; } - private float impactTolerance; - public readonly PriceInfo DefaultPrice; - private readonly Dictionary locationPrices; + private PriceInfo defaultPrice; + public PriceInfo DefaultPrice => defaultPrice; + + private ImmutableDictionary locationPrices; /// /// Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true, the character /// has to be within the trigger to interact. If it's set to false, having the cursor within the trigger is enough. /// - public List Triggers; + public ImmutableArray Triggers { get; private set; } + private ImmutableDictionary treatmentSuitability; private readonly List fabricationRecipeElements = new List(); - private readonly Dictionary treatmentSuitability = new Dictionary(); - /// /// Is this prefab overriding a prefab in another content package /// - public bool IsOverride; + public bool IsOverride => Prefabs.IsOverride(this); - public readonly ItemPrefab VariantOf; + private readonly XElement originalElement; + public ContentXElement ConfigElement { get; private set; } - public XElement ConfigElement - { - get; - private set; - } + public ImmutableArray DeconstructItems { get; private set; } - public List DeconstructItems - { - get; - private set; - } + public ImmutableDictionary FabricationRecipes { get; private set; } - public List FabricationRecipes - { - get; - private set; - } + public float DeconstructTime { get; private set; } - public float DeconstructTime - { - get; - private set; - } - - public bool AllowDeconstruct - { - get; - private set; - } - - //how close the Character has to be to the item to pick it up - [Serialize(120.0f, false)] - public float InteractDistance - { - get; - private set; - } - - // this can be used to allow items which are behind other items tp - [Serialize(0.0f, false)] - public float InteractPriority - { - get; - private set; - } - - [Serialize(false, false)] - public bool InteractThroughWalls - { - get; - private set; - } - - [Serialize(false, false, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] - public bool HideConditionBar { get; set; } - - [Serialize(false, false, description: "Hides the condition displayed in the item's tooltip.")] - public bool HideConditionInTooltip { get; set; } - - //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item - //if false, trigger areas define areas that can be used to highlight the item - [Serialize(true, false)] - public bool RequireBodyInsideTrigger - { - get; - private set; - } - - //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger - [Serialize(false, false)] - public bool RequireCursorInsideTrigger - { - get; - private set; - } - - //if true then players can only highlight the item if its targeted for interaction by a campaign event - [Serialize(false, false)] - public bool RequireCampaignInteract - { - get; - private set; - } - - //should the camera focus on the item when selected - [Serialize(false, false)] - public bool FocusOnSelected - { - get; - private set; - } - - //the amount of "camera offset" when selecting the construction - [Serialize(0.0f, false)] - public float OffsetOnSelected - { - get; - private set; - } - - [Serialize(100.0f, false)] - public float Health - { - get; - private set; - } - - [Serialize(false, false)] - public bool AllowSellingWhenBroken - { - get; - private set; - } - - [Serialize(false, false)] - public bool Indestructible - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByExplosions - { - get; - private set; - } - - [Serialize(1f, false)] - public float ExplosionDamageMultiplier - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByProjectiles - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByMeleeWeapons - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByRepairTools - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByMonsters - { - get; - private set; - } - - [Serialize(false, false)] - public bool FireProof - { - get; - private set; - } - - [Serialize(false, false)] - public bool WaterProof - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float ImpactTolerance - { - get { return impactTolerance; } - set { impactTolerance = Math.Max(value, 0.0f); } - } - - [Serialize(0.0f, false)] - public float OnDamagedThreshold { get; set; } - - [Serialize(0.0f, false)] - public float SonarSize - { - get; - private set; - } - - [Serialize(false, false)] - public bool UseInHealthInterface - { - get; - private set; - } - - [Serialize(false, false)] - public bool DisableItemUsageWhenSelected - { - get; - private set; - } - - [Serialize("", false)] - public string CargoContainerIdentifier - { - get; - private set; - } - - [Serialize(false, false)] - public bool UseContainedSpriteColor - { - get; - private set; - } - - [Serialize(false, false)] - public bool UseContainedInventoryIconColor - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float AddedRepairSpeedMultiplier - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float AddedPickingSpeedMultiplier - { - get; - private set; - } - - [Serialize(false, false)] - public bool CannotRepairFail - { - get; - private set; - } - - [Serialize(null, false)] - public string EquipConfirmationText { get; set; } - - [Serialize(true, false, description: "Can the item be rotated in the submarine editor.")] - public bool AllowRotatingInEditor { get; set; } - - [Serialize(false, false)] - public bool ShowContentsInTooltip { get; private set; } + public bool AllowDeconstruct { get; private set; } //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. - public List PreferredContainers - { - get; - private set; - } = new List(); + public ImmutableArray PreferredContainers { get; private set; } public SwappableItem SwappableItem { @@ -594,344 +365,371 @@ namespace Barotrauma /// /// How likely it is for the item to spawn in a level of a given type. - /// Key = name of the LevelGenerationParameters (empty string = default value) + /// Key = name of the LevelGenerationParameters (empty string = default value) /* TODO: empty string = default value???? */ /// Value = commonness /// - public Dictionary LevelCommonness - { - get; - private set; - } = new Dictionary(); + public ImmutableDictionary LevelCommonness { get; private set; } - public Dictionary LevelQuantity + public readonly struct FixedQuantityResourceInfo { - get; - } = new Dictionary(); - - public struct FixedQuantityResourceInfo - { - public int ClusterQuantity { get; } - public int ClusterSize { get; } - public bool IsIslandSpecifc { get; } - public bool AllowAtStart { get; } + public readonly int ClusterQuantity; + public readonly int ClusterSize; + public readonly bool IsIslandSpecific; + public readonly bool AllowAtStart; public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart) { ClusterQuantity = clusterQuantity; ClusterSize = clusterSize; - IsIslandSpecifc = isIslandSpecific; + IsIslandSpecific = isIslandSpecific; AllowAtStart = allowAtStart; } } - [Serialize(true, false)] - public bool CanFlipX { get; private set; } - - [Serialize(true, false)] - public bool CanFlipY { get; private set; } - - [Serialize(false, false)] - public bool IsDangerous { get; private set; } + public ImmutableDictionary LevelQuantity { get; private set; } public bool CanSpriteFlipX { get; private set; } public bool CanSpriteFlipY { get; private set; } - private int maxStackSize; - [Serialize(1, false)] - public int MaxStackSize - { - get { return maxStackSize; } - set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } - } - - [Serialize(false, false)] - public bool AllowDroppingOnSwap { get; private set; } - - private readonly HashSet allowDroppingOnSwapWith = new HashSet(); - public IEnumerable AllowDroppingOnSwapWith - { - get { return allowDroppingOnSwapWith; } - } - - public Vector2 Size => size; - - public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); - /// /// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign. /// - public bool? AllowAsExtraCargo; + public bool? AllowAsExtraCargo { get; private set; } + + public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); /// /// Any item with a Price element in the definition can be sold everywhere. /// public bool CanBeSold => DefaultPrice != null; - public bool RandomDeconstructionOutput { get; } + public bool RandomDeconstructionOutput { get; private set; } - public int RandomDeconstructionOutputAmount { get; } + public int RandomDeconstructionOutputAmount { get; private set; } - public uint UIntIdentifier { get; set; } + private Sprite sprite; + public override Sprite Sprite => sprite; - public static void RemoveByFile(string filePath) => Prefabs.RemoveByFile(filePath); + public override string OriginalName { get; } - public static void LoadFromFile(ContentFile file) + private LocalizedString name; + public override LocalizedString Name => name; + + private ImmutableHashSet tags; + public override ImmutableHashSet Tags => tags; + + private ImmutableHashSet allowedLinks; + public override ImmutableHashSet AllowedLinks => allowedLinks; + + private MapEntityCategory category; + public override MapEntityCategory Category => category; + + private ImmutableHashSet aliases; + public override ImmutableHashSet Aliases => aliases; + + //how close the Character has to be to the item to pick it up + [Serialize(120.0f, IsPropertySaveable.No)] + public float InteractDistance { get; private set; } + + // this can be used to allow items which are behind other items tp + [Serialize(0.0f, IsPropertySaveable.No)] + public float InteractPriority { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool InteractThroughWalls { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] + public bool HideConditionBar { get; set; } + + [Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")] + public bool HideConditionInTooltip { get; set; } + + //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item + //if false, trigger areas define areas that can be used to highlight the item + [Serialize(true, IsPropertySaveable.No)] + public bool RequireBodyInsideTrigger { get; private set; } + + //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger + [Serialize(false, IsPropertySaveable.No)] + public bool RequireCursorInsideTrigger { get; private set; } + + //if true then players can only highlight the item if its targeted for interaction by a campaign event + [Serialize(false, IsPropertySaveable.No)] + public bool RequireCampaignInteract { - DebugConsole.Log("*** " + file.Path + " ***"); - RemoveByFile(file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - var rootElement = doc.Root; - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "item": - new ItemPrefab(rootElement, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - break; - case "items": - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var itemElement = element.GetChildElement("item"); - if (itemElement != null) - { - new ItemPrefab(itemElement, file.Path, true) - { - ContentPackage = file.ContentPackage, - IsOverride = true - }; - } - else - { - DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}"); - } - } - else - { - new ItemPrefab(element, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - } - } - break; - case "override": - var items = rootElement.GetChildElement("items"); - if (items != null) - { - foreach (var element in items.Elements()) - { - new ItemPrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage, - IsOverride = true - }; - } - } - foreach (var element in rootElement.GetChildElements("item")) - { - new ItemPrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage - }; - } - break; - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); - break; - } + get; + private set; } - public static void LoadAll(IEnumerable files) + //should the camera focus on the item when selected + [Serialize(false, IsPropertySaveable.No)] + public bool FocusOnSelected { get; private set; } + + //the amount of "camera offset" when selecting the construction + [Serialize(0.0f, IsPropertySaveable.No)] + public float OffsetOnSelected { get; private set; } + + [Serialize(100.0f, IsPropertySaveable.No)] + public float Health { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowSellingWhenBroken { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool Indestructible { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByExplosions { get; private set; } + + [Serialize(1f, IsPropertySaveable.No)] + public float ExplosionDamageMultiplier { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByProjectiles { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByMeleeWeapons { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByRepairTools { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByMonsters { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool FireProof { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool WaterProof { get; private set; } + + private float impactTolerance; + [Serialize(0.0f, IsPropertySaveable.No)] + public float ImpactTolerance { - DebugConsole.Log("Loading item prefabs: "); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - - //initialize item requirements for fabrication recipes - //(has to be done after all the item prefabs have been loaded, because the - //recipe ingredients may not have been loaded yet when loading the prefab) - InitFabricationRecipes(); + get { return impactTolerance; } + set { impactTolerance = Math.Max(value, 0.0f); } } - public static void InitFabricationRecipes() + [Serialize(0.0f, IsPropertySaveable.No)] + public float OnDamagedThreshold { get; set; } + + [Serialize(0.0f, IsPropertySaveable.No)] + public float SonarSize { - foreach (ItemPrefab itemPrefab in Prefabs) - { - itemPrefab.FabricationRecipes.Clear(); - foreach (XElement fabricationRecipe in itemPrefab.fabricationRecipeElements) - { - itemPrefab.FabricationRecipes.Add(new FabricationRecipe(fabricationRecipe, itemPrefab)); - } - } + get; + private set; } - public static string GenerateLegacyIdentifier(string name) + [Serialize(false, IsPropertySaveable.No)] + public bool UseInHealthInterface { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DisableItemUsageWhenSelected { get; private set; } + + [Serialize("", IsPropertySaveable.No)] + public string CargoContainerIdentifier { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool UseContainedSpriteColor { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool UseContainedInventoryIconColor { get; private set; } + + [Serialize(0.0f, IsPropertySaveable.No)] + public float AddedRepairSpeedMultiplier { - return "legacyitem_" + name.ToLowerInvariant().Replace(" ", ""); + get; + private set; } - public ItemPrefab(XElement element, string filePath, bool allowOverriding) + [Serialize(0.0f, IsPropertySaveable.No)] + public float AddedPickingSpeedMultiplier { - FilePath = filePath; - ConfigElement = element; + get; + private set; + } - originalName = element.GetAttributeString("name", ""); - name = originalName; - identifier = element.GetAttributeString("identifier", ""); + [Serialize(false, IsPropertySaveable.No)] + public bool CannotRepairFail + { + get; + private set; + } - string variantOf = element.GetAttributeString("variantof", ""); - if (!string.IsNullOrEmpty(variantOf)) + [Serialize(null, IsPropertySaveable.No)] + public string EquipConfirmationText { get; set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor.")] + public bool AllowRotatingInEditor { get; set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool ShowContentsInTooltip { get; private set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool CanFlipX { get; private set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool CanFlipY { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool IsDangerous { get; private set; } + + private int maxStackSize; + [Serialize(1, IsPropertySaveable.No)] + public int MaxStackSize + { + get { return maxStackSize; } + private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } + } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowDroppingOnSwap { get; private set; } + + public ImmutableHashSet AllowDroppingOnSwapWith { get; private set; } + + protected override Identifier DetermineIdentifier(XElement element) + { + Identifier identifier = base.DetermineIdentifier(element); + string originalName = element.GetAttributeString("name", ""); + if (identifier.IsEmpty && !string.IsNullOrEmpty(originalName)) { - ItemPrefab basePrefab = Find(null, variantOf); - if (basePrefab == null) - { - DebugConsole.ThrowError($"Failed to load the item variant \"{identifier}\" - could not find the base prefab \"{variantOf}\""); - } - else - { - VariantOf = basePrefab; - ConfigElement = element = CreateVariantXML(element, basePrefab); - } - } - - string categoryStr = element.GetAttributeString("category", "Misc"); - if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) - { - category = MapEntityCategory.Misc; - } - Category = category; - - var parentType = element.Parent?.GetAttributeString("itemtype", "") ?? string.Empty; - - //nameidentifier can be used to make multiple items use the same names and descriptions - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); - - //only used if the item doesn't have a name/description defined in the currently selected language - string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); - - //works the same as nameIdentifier, but just replaces the description - string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); - - if (string.IsNullOrEmpty(originalName)) - { - if (string.IsNullOrEmpty(nameIdentifier)) - { - name = TextManager.Get("EntityName." + identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; - } - else - { - name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; - } - } - else if (Category.HasFlag(MapEntityCategory.Legacy)) - { - // Legacy items use names as identifiers, so we have to define them in the xml. But we also want to support the translations. Therefore - if (string.IsNullOrEmpty(nameIdentifier)) - { - name = TextManager.Get("EntityName." + identifier, true) ?? originalName; - } - else - { - name = TextManager.Get("EntityName." + nameIdentifier, true) ?? originalName; - } - - if (string.IsNullOrWhiteSpace(identifier)) + string categoryStr = element.GetAttributeString("category", "Misc"); + if (Enum.TryParse(categoryStr, true, out MapEntityCategory category) && category.HasFlag(MapEntityCategory.Legacy)) { identifier = GenerateLegacyIdentifier(originalName); } } + return identifier; + } - if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) + public static Identifier GenerateLegacyIdentifier(string name) + { + return ($"legacyitem_{name.Replace(" ", "")}").ToIdentifier(); + } + + public ItemPrefab(ContentXElement element, ItemFile file) : base(element, file) + { + originalElement = element; + ConfigElement = element; + + OriginalName = element.GetAttributeString("name", ""); + name = OriginalName; + + VariantOf = element.VariantOf(); + + if (!VariantOf.IsEmpty) { return; } //don't even attempt to read the XML until the PrefabCollection readies up the parent to inherit from + + ParseConfigElement(variantOf: null); + } + + private string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf) + => subElement.DoesAttributeReferenceFileNameAlone("texture") + ? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path) + : ""; + + private void ParseConfigElement(ItemPrefab variantOf) + { + string categoryStr = ConfigElement.GetAttributeString("category", "Misc"); + this.category = Enum.TryParse(categoryStr, true, out MapEntityCategory category) + ? category + : MapEntityCategory.Misc; + + var parentType = ConfigElement.Parent?.GetAttributeIdentifier("itemtype", ""); + + //nameidentifier can be used to make multiple items use the same names and descriptions + Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", ""); + + //only used if the item doesn't have a name/description defined in the currently selected language + string fallbackNameIdentifier = ConfigElement.GetAttributeString("fallbacknameidentifier", ""); + + //works the same as nameIdentifier, but just replaces the description + Identifier descriptionIdentifier = ConfigElement.GetAttributeIdentifier("descriptionidentifier", ""); + + if (string.IsNullOrEmpty(OriginalName)) { - if (!string.IsNullOrEmpty(name)) - { - name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); - } + name = TextManager.Get(nameIdentifier.IsEmpty + ? $"EntityName.{Identifier}" + : $"EntityName.{nameIdentifier}", + $"EntityName.{fallbackNameIdentifier}"); + } + else if (Category.HasFlag(MapEntityCategory.Legacy)) + { + // Legacy items use names as identifiers, so we have to define them in the xml. But we also want to support the translations. Therefore + name = TextManager.Get(nameIdentifier.IsEmpty + ? $"EntityName.{Identifier}" + : $"EntityName.{nameIdentifier}"); + name = name.Fallback(OriginalName); } - name = GeneticMaterial.TryCreateName(this, element); - - if (string.IsNullOrEmpty(name)) + if (parentType == "wrecked") { - DebugConsole.ThrowError($"Unnamed item ({identifier}) in {filePath}!"); + name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } - DebugConsole.Log(" " + name); + name = GeneticMaterial.TryCreateName(this, ConfigElement); - Aliases = new HashSet - (element.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? - element.GetAttributeStringArray("Aliases", new string[0], convertToLowerInvariant: true)); - Aliases.Add(originalName.ToLowerInvariant()); + this.aliases = + (ConfigElement.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? + ConfigElement.GetAttributeStringArray("Aliases", Array.Empty(), convertToLowerInvariant: true)) + .ToImmutableHashSet() + .Add(OriginalName.ToLowerInvariant()); - Triggers = new List(); - DeconstructItems = new List(); - FabricationRecipes = new List(); + var triggers = new List(); + var deconstructItems = new List(); + var fabricationRecipes = new Dictionary(); + var treatmentSuitability = new Dictionary(); + var locationPrices = new Dictionary(); + var preferredContainers = new List(); DeconstructTime = 1.0f; - if (element.Attribute("allowasextracargo") != null) + if (ConfigElement.Attribute("allowasextracargo") != null) { - AllowAsExtraCargo = element.GetAttributeBool("allowasextracargo", false); + AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false); } - Tags = new HashSet(element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true)); + this.tags = ConfigElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); if (!Tags.Any()) { - Tags = new HashSet(element.GetAttributeStringArray("Tags", new string[0], convertToLowerInvariant: true)); + this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty()).ToImmutableHashSet(); } - if (element.Attribute("cargocontainername") != null) + if (ConfigElement.Attribute("cargocontainername") != null) { - DebugConsole.ThrowError("Error in item prefab \"" + name + "\" - cargo container should be configured using the item's identifier, not the name."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name."); } - SerializableProperty.DeserializeProperties(this, element); + SerializableProperty.DeserializeProperties(this, ConfigElement); - if (string.IsNullOrEmpty(Description)) + if (Description.IsNullOrEmpty()) { - if (!string.IsNullOrEmpty(descriptionIdentifier)) + if (descriptionIdentifier != Identifier.Empty) { - Description = TextManager.Get("EntityDescription." + descriptionIdentifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); } - else if (string.IsNullOrEmpty(nameIdentifier)) + else if (nameIdentifier == Identifier.Empty) { - Description = TextManager.Get("EntityDescription." + identifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{Identifier}"); } else { - Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); } } - var allowDroppingOnSwapWith = element.GetAttributeStringArray("allowdroppingonswapwith", new string[0]); - if (allowDroppingOnSwapWith.Any()) - { - AllowDroppingOnSwap = true; - foreach (string tag in allowDroppingOnSwapWith) - { - this.allowDroppingOnSwapWith.Add(tag); - } - } + var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); + AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); + AllowDroppingOnSwap = allowDroppingOnSwapWith.Any(); - foreach (XElement subElement in element.Elements()) + var levelCommonness = new Dictionary(); + var levelQuantity = new Dictionary(); + + foreach (ContentXElement subElement in ConfigElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - string spriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - spriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } + string spriteFolder = GetTexturePath(subElement, variantOf); CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); @@ -940,21 +738,20 @@ namespace Barotrauma if (subElement.Attribute("sourcerect") == null && subElement.Attribute("sheetindex") == null) { - DebugConsole.ThrowError("Warning - sprite sourcerect not configured for item \"" + Name + "\"!"); + DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!"); } - size = sprite.size; + Size = Sprite.size; - if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(Name)) + if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) { - sprite.Name = Name; + Sprite.Name = Name.Value; } - sprite.EntityID = identifier; + Sprite.EntityIdentifier = Identifier; break; case "price": - if (locationPrices == null) { locationPrices = new Dictionary(); } if (subElement.Attribute("baseprice") != null) { - foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out DefaultPrice)) + foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out this.defaultPrice)) { if (priceInfo == null) { continue; } locationPrices.Add(priceInfo.Item1, priceInfo.Item2); @@ -962,138 +759,18 @@ namespace Barotrauma } else if (subElement.Attribute("buyprice") != null) { - string locationType = subElement.GetAttributeString("locationtype", "").ToLowerInvariant(); - locationPrices.Add(locationType, new PriceInfo(subElement)); - } - break; - case "upgradeoverride": - { -#if CLIENT - var sprites = new List(); - foreach (XElement decorSprite in subElement.Elements()) - { - if (decorSprite.Name.ToString().Equals("decorativesprite", StringComparison.OrdinalIgnoreCase)) + Identifier locationType = subElement.GetAttributeIdentifier("locationtype", ""); + if (locationPrices.ContainsKey(locationType)) { - sprites.Add(new DecorativeSprite(decorSprite)); + DebugConsole.AddWarning($"Error in item prefab \"{ToString()}\": price for the location type \"{locationType}\" defined more than once."); + locationPrices[locationType] = new PriceInfo(subElement); + } + else + { + locationPrices.Add(locationType, new PriceInfo(subElement)); } } - UpgradeOverrideSprites.Add(subElement.GetAttributeString("identifier", ""), sprites); -#endif break; - } -#if CLIENT - case "upgradepreviewsprite": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true); - UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f); - } - break; - case "inventoryicon": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "minimapicon": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "infectedsprite": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "damagedinfectedsprite": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "brokensprite": - string brokenSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - brokenSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - var brokenSprite = new BrokenItemSprite( - new Sprite(subElement, brokenSpriteFolder, lazyLoad: true), - subElement.GetAttributeFloat("maxcondition", 0.0f), - subElement.GetAttributeBool("fadein", false), - subElement.GetAttributePoint("offset", Point.Zero)); - - int spriteIndex = 0; - for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++) - { - spriteIndex = i; - } - BrokenSprites.Insert(spriteIndex, brokenSprite); - break; - case "decorativesprite": - string decorativeSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - decorativeSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - int groupID = 0; - DecorativeSprite decorativeSprite = null; - if (subElement.Attribute("texture") == null) - { - groupID = subElement.GetAttributeInt("randomgroupid", 0); - } - else - { - decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); - DecorativeSprites.Add(decorativeSprite); - groupID = decorativeSprite.RandomGroupID; - } - if (!DecorativeSpriteGroups.ContainsKey(groupID)) - { - DecorativeSpriteGroups.Add(groupID, new List()); - } - DecorativeSpriteGroups[groupID].Add(decorativeSprite); - - break; - case "containedsprite": - string containedSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - containedSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - var containedSprite = new ContainedItemSprite(subElement, containedSpriteFolder, lazyLoad: true); - if (containedSprite.Sprite != null) - { - ContainedSprites.Add(containedSprite); - } - break; -#endif case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); AllowDeconstruct = true; @@ -1103,27 +780,39 @@ namespace Barotrauma { if (deconstructItem.Attribute("name") != null) { - DebugConsole.ThrowError("Error in item config \"" + Name + "\" - use item identifiers instead of names to configure the deconstruct items."); + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items."); continue; } - DeconstructItems.Add(new DeconstructItem(deconstructItem, identifier)); + deconstructItems.Add(new DeconstructItem(deconstructItem, Identifier)); } - RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, DeconstructItems.Count); + RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count); break; case "fabricate": case "fabricable": case "fabricableitem": - fabricationRecipeElements.Add(subElement); + var newRecipe = new FabricationRecipe(subElement, Identifier); + if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe)) + { + DebugConsole.ThrowError( + $"Error in item prefab \"{ToString()}\": " + + $"{prevRecipe.DisplayName} has the same hash as {newRecipe.DisplayName}. " + + $"This will cause issues with fabrication." + ); + } + else + { + fabricationRecipes.Add(newRecipe.RecipeHash, newRecipe); + } break; case "preferredcontainer": var preferredContainer = new PreferredContainer(subElement); if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0) { - DebugConsole.ThrowError($"Error in item prefab {Name}: preferred container has no preferences defined ({subElement})."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement})."); } else { - PreferredContainers.Add(preferredContainer); + preferredContainers.Add(preferredContainer); } break; case "swappableitem": @@ -1138,25 +827,25 @@ namespace Barotrauma Height = subElement.GetAttributeInt("height", 0) }; - Triggers.Add(trigger); + triggers.Add(trigger); break; case "levelresource": foreach (XElement levelCommonnessElement in subElement.GetChildElements("commonness")) { - string levelName = levelCommonnessElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelName = levelCommonnessElement.GetAttributeIdentifier("leveltype", ""); if (!levelCommonnessElement.GetAttributeBool("fixedquantity", false)) { - if (!LevelCommonness.ContainsKey(levelName)) + if (!levelCommonness.ContainsKey(levelName)) { - LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); + levelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); } } else { - if (!LevelQuantity.ContainsKey(levelName)) + if (!levelQuantity.ContainsKey(levelName)) { - LevelQuantity.Add(levelName, new FixedQuantityResourceInfo( + levelQuantity.Add(levelName, new FixedQuantityResourceInfo( levelCommonnessElement.GetAttributeInt("clusterquantity", 0), levelCommonnessElement.GetAttributeInt("clustersize", 0), levelCommonnessElement.GetAttributeBool("isislandspecific", false), @@ -1168,70 +857,79 @@ namespace Barotrauma case "suitabletreatment": if (subElement.Attribute("name") != null) { - DebugConsole.ThrowError("Error in item prefab \"" + Name + "\" - suitable treatments should be defined using item identifiers, not item names."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names."); } - string treatmentIdentifier = (subElement.GetAttributeString("identifier", null) ?? subElement.GetAttributeString("type", string.Empty)).ToLowerInvariant(); + Identifier treatmentIdentifier = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty)); float suitability = subElement.GetAttributeFloat("suitability", 0.0f); treatmentSuitability.Add(treatmentIdentifier, suitability); break; } } - // Set the default price in case the prices are defined in the old way - // with separate Price elements and there is no default price explicitly set +#if CLIENT + ParseSubElementsClient(ConfigElement, variantOf); +#endif + + this.Triggers = triggers.ToImmutableArray(); + this.DeconstructItems = deconstructItems.ToImmutableArray(); + this.FabricationRecipes = fabricationRecipes.ToImmutableDictionary(); + this.treatmentSuitability = treatmentSuitability.ToImmutableDictionary(); + this.locationPrices = locationPrices.ToImmutableDictionary(); + this.PreferredContainers = preferredContainers.ToImmutableArray(); + this.LevelCommonness = levelCommonness.ToImmutableDictionary(); + this.LevelQuantity = levelQuantity.ToImmutableDictionary(); + + // Backwards compatibility if (locationPrices != null && locationPrices.Any()) { - DefaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + this.defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); } - HideConditionInTooltip = element.GetAttributeBool("hideconditionintooltip", HideConditionBar); + HideConditionInTooltip = ConfigElement.GetAttributeBool("hideconditionintooltip", HideConditionBar); //backwards compatibility if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) { - Category = MapEntityCategory.Wrecked; + this.category = MapEntityCategory.Wrecked; Subcategory = "Thalamus"; } - if (sprite == null) + if (Sprite == null) { - DebugConsole.ThrowError("Item \"" + Name + "\" has no sprite!"); + DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!"); #if SERVER - sprite = new Sprite("", Vector2.Zero); - sprite.SourceRect = new Rectangle(0, 0, 32, 32); + this.sprite = new Sprite("", Vector2.Zero); + this.sprite.SourceRect = new Rectangle(0, 0, 32, 32); #else - sprite = new Sprite(TextureLoader.PlaceHolderTexture, null, null) + this.sprite = new Sprite(TextureLoader.PlaceHolderTexture, null, null) { Origin = TextureLoader.PlaceHolderTexture.Bounds.Size.ToVector2() / 2 }; #endif - size = sprite.size; - sprite.EntityID = identifier; + Size = Sprite.size; + Sprite.EntityIdentifier = Identifier; } - - if (string.IsNullOrEmpty(identifier)) + + if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - "Item prefab \"" + name + "\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + $"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); } #if DEBUG if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) { - if (!string.IsNullOrEmpty(originalName)) + if (!string.IsNullOrEmpty(OriginalName)) { - DebugConsole.AddWarning($"Item \"{(string.IsNullOrEmpty(identifier) ? name : identifier)}\" has a hard-coded name, and won't be localized to other languages."); + DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); } } #endif - AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList(); - - Prefabs.Add(this, allowOverriding); - this.CalculatePrefabUIntIdentifier(Prefabs); + this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty()).ToImmutableHashSet(); } - public float GetTreatmentSuitability(string treatmentIdentifier) + public float GetTreatmentSuitability(Identifier treatmentIdentifier) { return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f; } @@ -1239,7 +937,7 @@ namespace Barotrauma public PriceInfo GetPriceInfo(Location location) { if (location?.Type == null) { return null; } - var locationTypeId = location.Type.Identifier?.ToLowerInvariant(); + var locationTypeId = location.Type.Identifier; if (locationPrices != null && locationPrices.ContainsKey(locationTypeId)) { return locationPrices[locationTypeId]; @@ -1261,15 +959,15 @@ namespace Barotrauma (location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } - public static ItemPrefab Find(string name, string identifier) + public static ItemPrefab Find(string name, Identifier identifier) { - if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(identifier)) + if (string.IsNullOrEmpty(name) && identifier.IsEmpty) { throw new ArgumentException("Both name and identifier cannot be null."); } ItemPrefab prefab; - if (string.IsNullOrEmpty(identifier)) + if (identifier.IsEmpty) { //legacy support identifier = GenerateLegacyIdentifier(name); @@ -1284,12 +982,12 @@ namespace Barotrauma } if (prefab == null) { - prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(identifier)); + prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(identifier.Value)); } if (prefab == null) { - DebugConsole.ThrowError("Error loading item - item prefab \"" + name + "\" (identifier \"" + identifier + "\") not found."); + DebugConsole.ThrowError($"Error loading item - item prefab \"{name}\" (identifier \"{identifier}\") not found."); } return prefab; } @@ -1314,12 +1012,12 @@ namespace Barotrauma } } - public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) + public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) { - Dictionary priceLocations = new Dictionary(); + Dictionary priceLocations = new Dictionary(); if (locationPrices != null) { - foreach (KeyValuePair locationPrice in locationPrices) + foreach (KeyValuePair locationPrice in locationPrices) { PriceInfo priceInfo = locationPrice.Value; @@ -1340,16 +1038,16 @@ namespace Barotrauma return priceLocations.ToImmutableDictionary(); } - public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) + public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) { - Dictionary priceLocations = new Dictionary(); + Dictionary priceLocations = new Dictionary(); if (!CanBeSold && sellingImportant) { return priceLocations.ToImmutableDictionary(); } - foreach (KeyValuePair locationPrice in locationPrices) + foreach (KeyValuePair locationPrice in locationPrices) { PriceInfo priceInfo = locationPrice.Value; @@ -1379,7 +1077,7 @@ namespace Barotrauma static bool HasConditionRequirement(PreferredContainer pc) => pc.MinCondition > 0 || pc.MaxCondition < 100; } - public bool IsContainerPreferred(Item item, string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) + public bool IsContainerPreferred(Item item, Identifier[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; @@ -1394,101 +1092,29 @@ namespace Barotrauma private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; - public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); - public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); + public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); + public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); - private XElement CreateVariantXML(XElement variantElement, ItemPrefab basePrefab) + protected override void CreateInstance(Rectangle rect) { - XElement newElement = new XElement(variantElement.Name); - newElement.Add(basePrefab.ConfigElement.Attributes()); - newElement.Add(basePrefab.ConfigElement.Elements()); + throw new InvalidOperationException("Can't call ItemPrefab.CreateInstance"); + } - ReplaceElement(newElement, variantElement); + private bool disposed = false; + public override void Dispose() + { + if (disposed) { return; } + disposed = true; + Prefabs.Remove(this); + Item.RemoveByPrefab(this); + } - void ReplaceElement(XElement element, XElement replacement) - { - List elementsToRemove = new List(); - foreach (XAttribute attribute in replacement.Attributes()) - { - ReplaceAttribute(element, attribute); - } - foreach (XElement replacementSubElement in replacement.Elements()) - { - int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement); - System.Diagnostics.Debug.Assert(index > -1); - - int i = 0; - bool matchingElementFound = false; - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } - if (i == index) - { - if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements) - { - //if the replacement is empty (no attributes or child elements) - //remove the element from the variant - elementsToRemove.Add(subElement); - } - else - { - ReplaceElement(subElement, replacementSubElement); - } - matchingElementFound = true; - break; - } - i++; - } - if (!matchingElementFound) - { - element.Add(replacementSubElement); - } - } - elementsToRemove.ForEach(e => e.Remove()); - } - - void ReplaceAttribute(XElement element, XAttribute newAttribute) - { - XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); - if (existingAttribute == null) - { - element.Add(newAttribute); - return; - } - float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value); - if (newAttribute.Value.StartsWith('*')) - { - string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); - float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier); - if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.')) - { - existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture); - } - else - { - existingAttribute.Value = ((int)(value * multiplier)).ToString(); - } - } - else if (newAttribute.Value.StartsWith('+')) - { - string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); - float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition); - if (additionStr.Contains('.') || existingAttribute.Value.Contains('.')) - { - existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture); - } - else - { - existingAttribute.Value = ((int)(value + addition)).ToString(); - } - } - else - { - existingAttribute.Value = newAttribute.Value; - } - } - - return newElement; + public Identifier VariantOf { get; } + + public void InheritFrom(ItemPrefab parent) + { + ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ParseConfigElement(parent); } public override string ToString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index a5c4b8974..136178582 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -22,14 +22,14 @@ namespace Barotrauma public bool IgnoreInEditor { get; set; } - private string[] excludedIdentifiers; + private Identifier[] excludedIdentifiers; private RelationType type; public List statusEffects; - public string Msg; - public string MsgTag; + public LocalizedString Msg; + public Identifier MsgTag; /// /// Should broken (0 condition) items be excluded @@ -55,15 +55,11 @@ namespace Barotrauma { if (value == null) return; - Identifiers = value.Split(','); - for (int i = 0; i < Identifiers.Length; i++) - { - Identifiers[i] = Identifiers[i].Trim().ToLowerInvariant(); - } + Identifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); } } - public string[] Identifiers { get; private set; } + public Identifier[] Identifiers { get; private set; } public string JoinedExcludedIdentifiers { @@ -72,11 +68,7 @@ namespace Barotrauma { if (value == null) return; - excludedIdentifiers = value.Split(','); - for (int i = 0; i < excludedIdentifiers.Length; i++) - { - excludedIdentifiers[i] = excludedIdentifiers[i].Trim().ToLowerInvariant(); - } + excludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); } } @@ -84,28 +76,19 @@ namespace Barotrauma { if (item == null) { return false; } if (excludedIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; } - return Identifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id)); + return Identifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && item.Prefab.VariantOf == id)); } public bool MatchesItem(ItemPrefab itemPrefab) { if (itemPrefab == null) { return false; } if (excludedIdentifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id))) { return false; } - return Identifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id) || (AllowVariants && itemPrefab.VariantOf?.Identifier == id)); + return Identifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id) || (AllowVariants && !itemPrefab.VariantOf.IsEmpty && itemPrefab.VariantOf == id)); } - public RelatedItem(string[] identifiers, string[] excludedIdentifiers) + public RelatedItem(Identifier[] identifiers, Identifier[] excludedIdentifiers) { - for (int i = 0; i < identifiers.Length; i++) - { - identifiers[i] = identifiers[i].Trim().ToLowerInvariant(); - } - this.Identifiers = identifiers; - - for (int i = 0; i < excludedIdentifiers.Length; i++) - { - excludedIdentifiers[i] = excludedIdentifiers[i].Trim().ToLowerInvariant(); - } - this.excludedIdentifiers = excludedIdentifiers; + this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToArray(); + this.excludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToArray(); statusEffects = new List(); } @@ -178,22 +161,22 @@ namespace Barotrauma element.Add(new XAttribute("excludedidentifiers", JoinedExcludedIdentifiers)); } - if (!string.IsNullOrWhiteSpace(Msg)) { element.Add(new XAttribute("msg", string.IsNullOrEmpty(MsgTag) ? Msg : MsgTag)); } + if (!Msg.IsNullOrWhiteSpace()) { element.Add(new XAttribute("msg", MsgTag.IsEmpty ? Msg : MsgTag.Value)); } } - public static RelatedItem Load(XElement element, bool returnEmpty, string parentDebugName) + public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName) { - string[] identifiers; + Identifier[] identifiers; if (element.Attribute("name") != null) { //backwards compatibility + a console warning DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); - string[] itemNames = element.GetAttributeStringArray("name", new string[0]); + Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); //attempt to convert to identifiers and tags - List convertedIdentifiers = new List(); - foreach (string itemName in itemNames) + List convertedIdentifiers = new List(); + foreach (Identifier itemName in itemNames) { - var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName); + var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); if (matchingItem != null) { convertedIdentifiers.Add(matchingItem.Identifier); @@ -208,24 +191,24 @@ namespace Barotrauma } else { - identifiers = element.GetAttributeStringArray("items", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("item", null, convertToLowerInvariant: true); + identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); if (identifiers == null) { - identifiers = element.GetAttributeStringArray("identifiers", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("tags", null, convertToLowerInvariant: true); + identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); if (identifiers == null) { - identifiers = element.GetAttributeStringArray("identifier", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("tag", new string[0], convertToLowerInvariant: true); + identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); } } } - string[] excludedIdentifiers = element.GetAttributeStringArray("excludeditems", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludeditem", null, convertToLowerInvariant: true); + Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); if (excludedIdentifiers == null) { - excludedIdentifiers = element.GetAttributeStringArray("excludedidentifiers", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludedtags", null, convertToLowerInvariant: true); + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); if (excludedIdentifiers == null) { - excludedIdentifiers = element.GetAttributeStringArray("excludedidentifier", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludedtag", new string[0], convertToLowerInvariant: true); + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); } } @@ -257,24 +240,24 @@ namespace Barotrauma return null; } - ri.MsgTag = element.GetAttributeString("msg", ""); - string msg = TextManager.Get(ri.MsgTag, true); - if (msg == null) + ri.MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); + LocalizedString msg = TextManager.Get(ri.MsgTag); + if (!msg.Loaded) { - ri.Msg = ri.MsgTag; + ri.Msg = ri.MsgTag.Value; } else { #if CLIENT foreach (InputType inputType in Enum.GetValues(typeof(InputType))) { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBindText(inputType)); + msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); } ri.Msg = msg; #endif } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs index da8c59d63..b9fc28d5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs @@ -1,5 +1,10 @@ -using System; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; using System.Text; namespace Barotrauma @@ -8,7 +13,80 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); + private readonly ConstructorInfo constructor; + + private CoreEntityPrefab( + Identifier identifier, + ConstructorInfo constructor, + bool resizeHorizontal = false, + bool resizeVertical = false, + bool linkable = false, + IEnumerable allowedLinks = null, + IEnumerable aliases = null) + : base(identifier) + { + this.constructor = constructor; + this.Name = TextManager.Get($"EntityName.{identifier}"); + this.Description = TextManager.Get($"EntityDescription.{identifier}"); + this.ResizeHorizontal = resizeHorizontal; + this.ResizeVertical = resizeVertical; + this.Linkable = linkable; + this.AllowedLinks = (allowedLinks ?? Enumerable.Empty()).ToImmutableHashSet(); + this.Aliases = (aliases ?? Enumerable.Empty()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet(); + } + + public static void InitCorePrefabs() + { + CoreEntityPrefab ep = new CoreEntityPrefab( + "hull".ToIdentifier(), + typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + resizeHorizontal: true, + resizeVertical: true, + linkable: true, + allowedLinks: new Identifier[] { "hull".ToIdentifier() }); + Prefabs.Add(ep, false); + + ep = new CoreEntityPrefab( + "gap".ToIdentifier(), + typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + resizeHorizontal: true, + resizeVertical: true); + Prefabs.Add(ep, false); + + ep = new CoreEntityPrefab( + "waypoint".ToIdentifier(), + typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); + Prefabs.Add(ep, false); + + ep = new CoreEntityPrefab( + "spawnpoint".ToIdentifier(), + typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); + Prefabs.Add(ep, false); + } + + protected override void CreateInstance(Rectangle rect) + { + if (constructor == null) return; + object[] lobject = new object[] { this, rect }; + constructor.Invoke(lobject); + } + private bool disposed = false; + + public override Sprite Sprite => null; + + public override string OriginalName => Name.Value; + + public override LocalizedString Name { get; } + + public override ImmutableHashSet Tags { get; } = Enumerable.Empty().ToImmutableHashSet(); + + public override ImmutableHashSet AllowedLinks { get; } + + public override MapEntityCategory Category => MapEntityCategory.Structure; + + public override ImmutableHashSet Aliases { get; } + public override void Dispose() { if (disposed) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index de0f565e8..9e02177fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -18,45 +18,72 @@ namespace Barotrauma.MapCreatures.Behavior public readonly BallastFloraBehavior? ParentBallastFlora; public int ID = -1; - public Item ClaimedItem; + public Item? ClaimedItem; public int ClaimedItemId = -1; public float MaxHealth = 100f; - public float Health = 100f; + + private float health = 100; + public float Health + { + get { return health; } + set { health = MathHelper.Clamp(value, 0.0f, MaxHealth); } + } + + public float RemoveTimer = 60.0f; public bool SpawningItem; public Item? AttackItem; public bool IsRoot; + /// + /// Decorative branches that grow around the root + /// + public bool IsRootGrowth; public bool Removed; + public bool DisconnectedFromRoot; + public Hull? CurrentHull; public float Pulse = 1.0f; private bool inflate; private float pulseDelay = Rand.Range(0f, 3f); + public readonly BallastFloraBranch? ParentBranch; + /// + /// How far from the root this branch is + /// + public readonly int BranchDepth; + public float AccumulatedDamage; + public float DamageVisualizationTimer; + public Vector2 ShakeAmount; // Adjacent tiles, used to free up sides when this branch gets removed public readonly Dictionary Connections = new Dictionary(); - public BallastFloraBranch(BallastFloraBehavior? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) + public BallastFloraBranch(BallastFloraBehavior? parent, BallastFloraBranch? parentBranch, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) : base(null, position, type, flowerConfig, leafConfig, rect) { + ParentBranch = parentBranch; ParentBallastFlora = parent; + if (parentBranch != null) + { + BranchDepth = parentBranch.BranchDepth + 1; + } } public void UpdateHealth() { if (MaxHealth <= Health) { return; } Color healthColor = Color.White * (1.0f - Health / MaxHealth); - HealthColor = healthColor; + HealthColor = Color.Lerp(HealthColor, healthColor, 0.05f); } public void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay) { - if (ParentBallastFlora == null) { return; } + if (ParentBallastFlora == null || DisconnectedFromRoot) { return; } if (pulseDelay > 0) { @@ -91,7 +118,7 @@ namespace Barotrauma.MapCreatures.Behavior public List> debugSearchLines = new List>(); #endif - private static List _entityList = new List(); + private readonly static List _entityList = new List(); public static IEnumerable EntityList => _entityList; public enum NetworkHeader @@ -101,119 +128,126 @@ namespace Barotrauma.MapCreatures.Behavior BranchCreate, BranchRemove, BranchDamage, - Infect + Infect, + Remove } public enum AttackType { Fire, Explosives, - Other + Other, + CutFromRoot } public struct AITarget { - public string[] Tags; + public Identifier[] Tags; public int Priority; - public AITarget(XElement element) + public AITarget(ContentXElement element) { - Tags = element.GetAttributeStringArray("tags", new string[0]); + Tags = element.GetAttributeIdentifierArray("tags", Array.Empty())!; Priority = element.GetAttributeInt("priority", 0); } public bool Matches(Item item) - { - foreach (string tag in item.GetTags()) + { + foreach (Identifier targetTag in Tags) { - foreach (string targetTag in Tags) - { - if (tag == targetTag) { return true; } - } + if (item.HasTag(targetTag)) { return true; } } - return false; } } - [Serialize(0.25f, true, "Scale of the branches")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the branches.")] public float BaseBranchScale { get; set; } - [Serialize(0.25f, true, "Scale of the flowers")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the flowers.")] public float BaseFlowerScale { get; set; } - [Serialize(0.5f, true, "Scale of the leaves")] + [Serialize(0.5f, IsPropertySaveable.Yes, "Scale of the leaves.")] public float BaseLeafScale { get; set; } - [Serialize(0.33f, true, "Chance for a flower to appear on the branch")] + [Serialize(0.33f, IsPropertySaveable.Yes, "Chance for a flower to appear on a branch.")] public float FlowerProbability { get; set; } - [Serialize(0.7f, true, "Change for leaves to appear for the branch")] + [Serialize(0.7f, IsPropertySaveable.Yes, "Chance for leaves to appear on a branch.")] public float LeafProbability { get; set; } - [Serialize(3f, true, "Delay between pulses")] + [Serialize(3f, IsPropertySaveable.Yes, "Delay between pulses.")] public float PulseDelay { get; set; } - [Serialize(3f, true, "How fast the flower inflates during a pulse")] + [Serialize(3f, IsPropertySaveable.Yes, "How fast the flower inflates during a pulse.")] public float PulseInflateSpeed { get; set; } - [Serialize(1f, true, "How fast the flower deflates")] + [Serialize(1f, IsPropertySaveable.Yes, "How fast the flower deflates.")] public float PulseDeflateSpeed { get; set; } - [Serialize(32, true, "How many vines must grow before the plant breaks thru the wall")] + [Serialize(32, IsPropertySaveable.Yes, "How many vines must grow before the plant breaks through the wall.")] public int BreakthroughPoint { get; set; } - [Serialize(false, true, "Has the plant grown large enough to expose itself")] + [Serialize(false, IsPropertySaveable.Yes, "Has the plant grown large enough to expose itself.")] public bool HasBrokenThrough { get; set; } - [Serialize(300, true, "How far the ballast flora can detect items")] + [Serialize(300, IsPropertySaveable.Yes, "How far the ballast flora can detect items from.")] public int Sight { get; set; } - [Serialize(100, true, "How much health the branches have")] + [Serialize(100, IsPropertySaveable.Yes, "How much health the branches have.")] public int BranchHealth { get; set; } - [Serialize(400, true, "How much health the stem has")] - public int StemHealth { get; set; } + [Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")] + public int RootHealth { get; set; } - [Serialize(300f, true, "How much power the ballast flora takes from junction boxes")] + [Serialize(0.0005f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")] + public float HealthRegenPerBranch { get; set; } + + [Serialize(30, IsPropertySaveable.Yes, "How far away from the root branches can regenerate health (in number of branches). The amount of regen decreases lineary further from the root.")] + public int MaxBranchHealthRegenDistance { get; set; } + + [Serialize("255,255,255,255", IsPropertySaveable.Yes)] + public Color RootColor { get; set; } + + [Serialize(300f, IsPropertySaveable.Yes, "How much power the ballast flora takes from junction boxes.")] public float PowerConsumptionMin { get; set; } - [Serialize(3000f, true, "How much the power drain spikes")] + [Serialize(3000f, IsPropertySaveable.Yes, "How much the power drain spikes.")] public float PowerConsumptionMax { get; set; } - [Serialize(10f, true, "How long it takes for power drain to wind down")] + [Serialize(10f, IsPropertySaveable.Yes, "How long it takes for power drain to wind down.")] public float PowerConsumptionDuration { get; set; } - [Serialize(250f, true, "How much power does it take to accelerate growth")] + [Serialize(250f, IsPropertySaveable.Yes, "How much power does it take to accelerate growth.")] public float PowerRequirement { get; set; } - [Serialize(5f, true, "Maximum anger, anger increases when the plant gets damaged and increases growth speed")] + [Serialize(5f, IsPropertySaveable.Yes, "Maximum anger, anger increases when the plant gets damaged and increases growth speed.")] public float MaxAnger { get; set; } - [Serialize(10000f, true, "Maximum power buffer")] + [Serialize(10000f, IsPropertySaveable.Yes, "Maximum power buffer.")] public float MaxPowerCapacity { get; set; } - [Serialize("", true, "Item prefab that is spawned when threatened")] - public string AttackItemPrefab { get; set; } = ""; + [Serialize("", IsPropertySaveable.Yes, "Item prefab that is spawned when threatened.")] + public Identifier AttackItemPrefab { get; set; } = Identifier.Empty; - [Serialize(0.8f, true, "How resistant the ballast flora is to exlposives before it blooms")] + [Serialize(0.8f, IsPropertySaveable.Yes, "How resistant the ballast flora is to explosives before it blooms.")] public float ExplosionResistance { get; set; } - [Serialize(5f, true, "How much damage is taken from open fires")] + [Serialize(5f, IsPropertySaveable.Yes, "How much damage is taken from open fires.")] public float FireVulnerability { get; set; } - [Serialize(0.5f, true, "How much resistance against fire is gained while submerged.")] + [Serialize(0.5f, IsPropertySaveable.Yes, "How much resistance against fire is gained while submerged.")] public float SubmergedWaterResistance { get; set; } - [Serialize(0.8f, true, "What depth the branches will be drawn on")] + [Serialize(0.8f, IsPropertySaveable.Yes, "What depth the branches will be drawn on.")] public float BranchDepth { get; set; } - [Serialize("", true, "What sound to play when the ballast flora bursts thru walls")] + [Serialize("", IsPropertySaveable.Yes, "What sound to play when the ballast flora bursts through walls.")] public string BurstSound { get; set; } = ""; private float availablePower; - [Serialize(0f, true, "How much power the ballast flora has stored.")] + [Serialize(0f, IsPropertySaveable.Yes, "How much power the ballast flora has stored.")] public float AvailablePower { get => availablePower; @@ -222,7 +256,7 @@ namespace Barotrauma.MapCreatures.Behavior private float anger; - [Serialize(1f, true, "How enraged the flora is, affects how fast it grows.")] + [Serialize(1f, IsPropertySaveable.Yes, "How enraged the flora is, affects how fast it grows.")] public float Anger { get => anger; @@ -235,13 +269,13 @@ namespace Barotrauma.MapCreatures.Behavior public BallastFloraPrefab Prefab { get; private set; } - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } public Vector2 Offset; - public readonly List ClaimedTargets = new List(); - public readonly List ClaimedJunctionBoxes = new List(); - public readonly List ClaimedBatteries = new List(); + public readonly HashSet ClaimedTargets = new HashSet(); + public readonly HashSet ClaimedJunctionBoxes = new HashSet(); + public readonly HashSet ClaimedBatteries = new HashSet(); public readonly Dictionary IgnoredTargets = new Dictionary(); private readonly List> tempClaimedTargets = new List>(); @@ -252,11 +286,12 @@ namespace Barotrauma.MapCreatures.Behavior public float PowerConsumptionTimer; private float defenseCooldown, toxinsCooldown, fireCheckCooldown; - private float damageIndicatorTimer, selfDamageTimer, toxinsTimer; + private float selfDamageTimer, toxinsTimer; private readonly List branchesVulnerableToFire = new List(); public readonly List Branches = new List(); + private BallastFloraBranch? root; private readonly List bodies = new List(); public readonly BallastFloraStateMachine StateMachine; @@ -319,15 +354,15 @@ namespace Barotrauma.MapCreatures.Behavior SerializableProperties = SerializableProperty.DeserializeProperties(this, prefab.Element); LoadPrefab(prefab.Element); StateMachine = new BallastFloraStateMachine(this); - if (firstGrowth) { GenerateStem(); } + if (firstGrowth) { GenerateRoot(); } _entityList.Add(this); } - partial void LoadPrefab(XElement element); + partial void LoadPrefab(ContentXElement element); - public void LoadTargets(XElement element) + public void LoadTargets(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { Targets.Add(new AITarget(subElement)); } @@ -358,6 +393,10 @@ namespace Barotrauma.MapCreatures.Behavior { be.Add(new XAttribute("claimed", (int)(branch.ClaimedItem?.ID ?? -1))); } + if (branch.ParentBranch != null) + { + be.Add(new XAttribute("parentbranch", (int)(branch.ParentBranch?.ID ?? -1))); + } saveElement.Add(be); } @@ -382,7 +421,7 @@ namespace Barotrauma.MapCreatures.Behavior { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); Offset = element.GetAttributeVector2("offset", Vector2.Zero); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -412,8 +451,15 @@ namespace Barotrauma.MapCreatures.Behavior int sides = getInt("sides"); int blockedSides = getInt("blockedsides"); int claimedId = branchElement.GetAttributeInt("claimed", -1); + int parentBranchId = branchElement.GetAttributeInt("parentbranch", -1); - BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig)) + BallastFloraBranch? parentBranch = null; + if (parentBranchId > -1) + { + parentBranch = Branches[parentBranchId]; + } + + BallastFloraBranch newBranch = new BallastFloraBranch(this, parentBranch, pos, VineTileType.CrossJunction, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig)) { ID = id, Health = health, @@ -422,6 +468,7 @@ namespace Barotrauma.MapCreatures.Behavior BlockedSides = (TileSide) blockedSides, IsRoot = isRoot }; + if (newBranch.IsRoot) { root = newBranch; } if (claimedId > -1) { @@ -437,6 +484,15 @@ namespace Barotrauma.MapCreatures.Behavior public void Update(float deltaTime) { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + if (Branches.Count == 0) + { + Remove(); + return; + } + } + foreach (BallastFloraBranch branch in Branches) { branch.UpdateScale(deltaTime); @@ -446,38 +502,25 @@ namespace Barotrauma.MapCreatures.Behavior #endif } - if (damageIndicatorTimer <= 0) - { - foreach (BallastFloraBranch branch in Branches) - { - if (branch.AccumulatedDamage > 0) - { - -#if CLIENT - CreateDamageParticle(branch, branch.AccumulatedDamage); - - if (GameMain.DebugDraw) - { - var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position; - GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1); - } -#elif SERVER - SendNetworkMessage(this, NetworkHeader.BranchDamage, branch, branch.AccumulatedDamage); -#endif - } - - branch.AccumulatedDamage = 0f; - } - - damageIndicatorTimer = 1f; - } - - damageIndicatorTimer -= deltaTime; + UpdateDamage(deltaTime); UpdatePowerDrain(deltaTime); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (root != null && HealthRegenPerBranch > 0.0f) + { + float healAmount = Branches.Count(b => !b.IsRoot && !b.IsRootGrowth && !b.DisconnectedFromRoot) * HealthRegenPerBranch; + + foreach (BallastFloraBranch branch in Branches) + { + if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; } + float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount; + if (branchHealAmount <= 0.0f) { continue; } + branch.Health += branchHealAmount; + branch.AccumulatedDamage -= branchHealAmount; + } + } StateMachine.Update(deltaTime); if (HasBrokenThrough) @@ -512,12 +555,12 @@ namespace Barotrauma.MapCreatures.Behavior // This entire scope is probably very heavy for GC, need to experiment if (toxinsTimer > 0.1f) { - if (!string.IsNullOrWhiteSpace(AttackItemPrefab)) + if (!AttackItemPrefab.IsEmpty) { Dictionary> branches = new Dictionary>(); foreach (BallastFloraBranch branch in Branches) { - if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0) { continue; } + if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0 || branch.DisconnectedFromRoot) { continue; } if (branches.TryGetValue(branch.CurrentHull, out List? list)) { @@ -534,11 +577,12 @@ namespace Barotrauma.MapCreatures.Behavior List list = branches[hull]; if (!list.Any(HasAcidEmitter)) { - BallastFloraBranch randomBranch = branches[hull].GetRandom(); + BallastFloraBranch randomBranch = branches[hull].GetRandomUnsynced(); randomBranch.SpawningItem = true; ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab); - Entity.Spawner?.AddToSpawnQueue(prefab, Parent.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item => + #warning TODO: Parent needs a nullability sanity check + Entity.Spawner?.AddItemToSpawnQueue(prefab, Parent!.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item => { randomBranch.AttackItem = item; randomBranch.SpawningItem = false; @@ -563,38 +607,49 @@ namespace Barotrauma.MapCreatures.Behavior } } + partial void UpdateDamage(float deltaTime); + + private readonly List toBeRemoved = new List(); private void UpdateSelfDamage(float deltaTime) { if (selfDamageTimer <= 0) { - bool hasRoot = false; - foreach (BallastFloraBranch branch in Branches) - { - if (branch.IsRoot) - { - hasRoot = true; - break; - } - } - - if (!hasRoot) - { - Kill(); - return; - } - if (!HasBrokenThrough && !CanGrowMore()) { Branches.ForEachMod(branch => { - float maxHealth = branch.IsRoot ? StemHealth : BranchHealth; + float maxHealth = branch.IsRoot ? RootHealth : BranchHealth; DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other); }); } - selfDamageTimer = 1f; } + toBeRemoved.Clear(); + foreach (BallastFloraBranch branch in Branches) + { + if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)) + { + DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot); + } + if (branch.Health <= 0.0f) + { + if (branch.ClaimedItem != null) + { + RemoveClaim(branch.ClaimedItem); + branch.ClaimedItem = null; + } + branch.RemoveTimer -= deltaTime; + if (branch.RemoveTimer <= 0.0f) + { + toBeRemoved.Add(branch); + } + } + } + foreach (BallastFloraBranch branch in toBeRemoved) + { + RemoveBranch(branch); + } selfDamageTimer -= deltaTime; } @@ -675,20 +730,25 @@ namespace Barotrauma.MapCreatures.Behavior branch.CurrentHull = Hull.FindHull(GetWorldPosition() + branch.Position, Parent, true); } - private void GenerateStem() + private void GenerateRoot() { - BallastFloraBranch stem = new BallastFloraBranch(this, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig) + if (root != null) + { + DebugConsole.ThrowError("Error in ballast flora: tried to grow a root even though root has already been created.\n" + Environment.StackTrace); + } + + root = new BallastFloraBranch(this, null, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right, GrowthStep = 1f, - Health = StemHealth, - MaxHealth = StemHealth, + MaxHealth = RootHealth, + Health = RootHealth, IsRoot = true, CurrentHull = Parent }; - - Branches.Add(stem); - CreateBody(stem); + + Branches.Add(root); + CreateBody(root); } public float GetGrowthSpeed(float deltaTime) @@ -704,19 +764,19 @@ namespace Barotrauma.MapCreatures.Behavior return deltaTime; } - public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List result) + public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List result, bool isRootGrowth = false, Vector2? forcePosition = null) { result = new List(); - if (parent.IsSideBlocked(side)) { return false; } + if (!isRootGrowth && parent.IsSideBlocked(side)) { return false; } - Vector2 pos = parent.AdjacentPositions[side]; + Vector2 pos = forcePosition ?? parent.AdjacentPositions[side]; Rectangle rect = VineTile.CreatePlantRect(pos); - if (CollidesWithWorld(rect)) + if (CollidesWithWorld(rect, checkOtherBranches: !isRootGrowth)) { parent.BlockedSides |= side; parent.FailedGrowthAttempts++; - return false; + return false; } FoliageConfig flowerConfig = FoliageConfig.EmptyConfig; @@ -732,21 +792,22 @@ namespace Barotrauma.MapCreatures.Behavior leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, 0.5f, 1.0f); } - BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect) + BallastFloraBranch newBranch = new BallastFloraBranch(this, parent, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect) { ID = CreateID(), + MaxHealth = BranchHealth, Health = BranchHealth, - MaxHealth = BranchHealth + IsRootGrowth = isRootGrowth }; SetHull(newBranch); if (newBranch.CurrentHull == null || newBranch.CurrentHull.Submarine != Parent.Submarine) { - parent.BlockedSides |= side; + if (!isRootGrowth) { parent.BlockedSides |= side; } parent.FailedGrowthAttempts++; return false; - } + } UpdateConnections(newBranch, parent); @@ -760,12 +821,28 @@ namespace Barotrauma.MapCreatures.Behavior GrowthWarps--; } + int rootGrowthCount = Branches.Count(b => b.IsRootGrowth); + if (rootGrowthCount < GetDesiredRootGrowthAmount()) + { + if (root != null) + { + Vector2 rootGrowthPos = Rand.Vector(rootGrowthCount * Rand.Range(3.0f, 5.0f)); + TryGrowBranch(root, TileSide.None, out List newRootGrowth, isRootGrowth: true, forcePosition: rootGrowthPos); + } + } + #if SERVER SendNetworkMessage(this, NetworkHeader.BranchCreate, newBranch, parent.ID); #endif return true; } + private int GetDesiredRootGrowthAmount() + { + if (root == null) { return 0; } + return MathHelper.Clamp(Branches.Count(b => !b.IsRootGrowth && b.Health > 0) / 20, 3, 30); + } + public bool BranchContainsTarget(BallastFloraBranch branch, Item target) { Rectangle worldRect = branch.Rect; @@ -894,6 +971,14 @@ namespace Barotrauma.MapCreatures.Behavior public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null) { float damage = amount; + + if (type != AttackType.Other && type != AttackType.CutFromRoot) + { + branch.DamageVisualizationTimer = 1.0f; + } + + if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } + // damage is handled server side currently if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -922,12 +1007,10 @@ namespace Barotrauma.MapCreatures.Behavior } } - branch.AccumulatedDamage += damage; - branch.Health -= damage; - - if (type != AttackType.Other) + if (type != AttackType.Other && type != AttackType.CutFromRoot) { + branch.AccumulatedDamage += damage; Anger += damage * 0.001f; } @@ -935,30 +1018,13 @@ namespace Barotrauma.MapCreatures.Behavior GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage); #endif - if (branch.Health < 0) + if (branch.Health <= 0 && type != AttackType.CutFromRoot) { RemoveBranch(branch); + if (branch.IsRoot) { Kill(); } } } - public void Remove() - { - foreach (Body body in bodies) - { - GameMain.World.Remove(body); - } - - Parent.BallastFlora = null; - Branches.Clear(); - - foreach (Item target in ClaimedTargets) - { - target.Infector = null; - } - - _entityList.Remove(this); - } - public void RemoveBranch(BallastFloraBranch branch) { bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -969,6 +1035,21 @@ namespace Barotrauma.MapCreatures.Behavior Branches.Remove(branch); branch.Removed = true; + bool foundDisconnected = false; + do + { + foundDisconnected = false; + foreach (BallastFloraBranch otherBranch in Branches) + { + if (otherBranch.ParentBranch == null || otherBranch.DisconnectedFromRoot) { continue; } + if (otherBranch.ParentBranch.Removed || otherBranch.ParentBranch.DisconnectedFromRoot) + { + otherBranch.DisconnectedFromRoot = true; + foundDisconnected = true; + } + } + } while (foundDisconnected); + bodies.ForEachMod(body => { if (body.UserData == branch) @@ -995,11 +1076,21 @@ namespace Barotrauma.MapCreatures.Behavior }); #if CLIENT - CreateDeathParticle(branch); + CreateDeathParticle(branch, 1.0f); #endif if (isClient) { return; } + int rootGrowthCount = Branches.Count(b => b.IsRootGrowth); + if (rootGrowthCount > GetDesiredRootGrowthAmount()) + { + var rootGrowth = Branches.LastOrDefault(b => b.IsRootGrowth); + if (rootGrowth != null) + { + RemoveBranch(rootGrowth); + } + } + if (branch.ClaimedItem != null) { RemoveClaim(branch.ClaimedItem); @@ -1050,8 +1141,10 @@ namespace Barotrauma.MapCreatures.Behavior public void Kill() { - Branches.ForEachMod(RemoveBranch); - Parent.BallastFlora = null; + foreach (var branch in Branches) + { + branch.DisconnectedFromRoot = true; + } foreach (Item target in ClaimedTargets) { @@ -1059,6 +1152,19 @@ namespace Barotrauma.MapCreatures.Behavior } StateMachine?.State?.Exit(); +#if SERVER + SendNetworkMessage(this, NetworkHeader.Kill); +#endif + } + + public void Remove() + { + Kill(); + + Branches.ForEachMod(RemoveBranch); + Branches.Clear(); + toBeRemoved.Clear(); + Parent.BallastFlora = null; // clean up leftover (can probably be removed) foreach (Body body in bodies) @@ -1067,8 +1173,9 @@ namespace Barotrauma.MapCreatures.Behavior GameMain.World.Remove(body); } + _entityList.Remove(this); #if SERVER - SendNetworkMessage(this, NetworkHeader.Kill); + SendNetworkMessage(this, NetworkHeader.Remove); #endif } @@ -1093,9 +1200,9 @@ namespace Barotrauma.MapCreatures.Behavior private bool CanGrowMore() => Branches.Any(b => b.CanGrowMore()); - private bool CollidesWithWorld(Rectangle rect) + private bool CollidesWithWorld(Rectangle rect, bool checkOtherBranches = true) { - if (Branches.Any(g => g.Rect.Contains(rect))) { return true; } + if (checkOtherBranches && Branches.Any(g => g.Rect.Contains(rect))) { return true; } Rectangle worldRect = rect; worldRect.Location = (Parent.Position + Offset).ToPoint() + worldRect.Location; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs index e053c3104..232a7563b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs @@ -4,125 +4,28 @@ using System.Xml.Linq; namespace Barotrauma { - class BallastFloraPrefab : IPrefab, IDisposable + class BallastFloraPrefab : Prefab { public string OriginalName { get; } - public string Identifier { get; } - public string FilePath { get; } - public XElement Element { get; } - - public ContentPackage ContentPackage { get; private set; } + public LocalizedString DisplayName { get; } + public ContentXElement Element { get; } public bool Disposed; public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private BallastFloraPrefab(XElement element, string filePath, bool isOverride) + public BallastFloraPrefab(ContentXElement element, BallastFloraFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - Identifier = element.GetAttributeString("identifier", ""); OriginalName = element.GetAttributeString("name", ""); + DisplayName = TextManager.Get(Identifier).Fallback(OriginalName); Element = element; - FilePath = filePath; - Prefabs.Add(this, isOverride); } - public static BallastFloraPrefab Find(string idenfitier) + public static BallastFloraPrefab Find(Identifier identifier) { - return !string.IsNullOrWhiteSpace(idenfitier) ? Prefabs.Find(prefab => prefab.Identifier == idenfitier) : null; + return Prefabs.ContainsKey(identifier) ? Prefabs[identifier] : null; } - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading map creature prefabs: "); - - foreach (ContentFile file in files) { LoadFromFile(file); } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - - var rootElement = doc?.Root; - if (rootElement == null) { return; } - - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "ballastflorabehavior": - { - new BallastFloraPrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage }; - break; - } - case "ballastflorabehaviors": - { - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - XElement upgradeElement = element.GetChildElement("mapcreature"); - if (upgradeElement != null) - { - new BallastFloraPrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage }; - } - else - { - DebugConsole.ThrowError($"Cannot find a map creature element from the children of the override element defined in {file.Path}"); - } - } - else - { - if (element.Name.ToString().Equals("mapcreature", StringComparison.OrdinalIgnoreCase)) - { - new BallastFloraPrefab(element, file.Path, false) { ContentPackage = file.ContentPackage }; - } - } - } - - break; - } - case "override": - { - XElement mapCreatures = rootElement.GetChildElement("ballastflorabehaviors"); - if (mapCreatures != null) - { - foreach (XElement element in mapCreatures.Elements()) - { - new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - } - - foreach (XElement element in rootElement.GetChildElements("ballastflorabehavior")) - { - new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - - break; - } - default: - { - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " + - "Valid elements are: \"MapCreature\", \"MapCreatures\" and \"Override\"."); - break; - } - } - } - - private void Dispose(bool disposing) - { - if (!Disposed) - { - if (disposing) - { - Prefabs.Remove(this); - } - } - - Disposed = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public override void Dispose() { } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs index ab939cc7e..f09f2a98b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs @@ -18,7 +18,7 @@ namespace Barotrauma.MapCreatures.Behavior private bool tryDrown; private readonly Character attacker; - public DefendWithPumpState(BallastFloraBranch branch, List items, Character attacker) + public DefendWithPumpState(BallastFloraBranch branch, IEnumerable items, Character attacker) { targetBranch = branch; this.attacker = attacker; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs index ff3ac93bb..9246cb7e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs @@ -59,11 +59,14 @@ namespace Barotrauma.MapCreatures.Behavior protected virtual void Grow() { - List newTiles = GrowRandomly(); + List newBranches = GrowRandomly(); #if DEBUG Behavior.debugSearchLines.Clear(); #endif - if (newTiles.Any(TryScanTargets)) { return; } + foreach (var branch in newBranches) + { + TryScanTargets(branch); + } } public void UpdateIgnoredTargets() @@ -85,23 +88,20 @@ namespace Barotrauma.MapCreatures.Behavior private List GrowRandomly() { - List newBranches = new List(); - List newList = new List(Behavior.Branches); - foreach (BallastFloraBranch branch in newList) - { - if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; } + List availableBranches = Behavior.Branches.Where(b => !b.DisconnectedFromRoot && b.FailedGrowthAttempts <= 8 && b.CanGrowMore()).ToList(); + if (availableBranches.Count == 0) { return availableBranches; } - if (Rand.Range(0, Behavior.Branches.Count(tile => tile.CanGrowMore())) != 0) { continue; } + //prefer growing from the branches furthest from the root (ones with the largest branch depth) + var branch = ToolBox.SelectWeightedRandom(availableBranches, b => (float)b.BranchDepth, Rand.RandSync.Unsynced); - TileSide side = branch.GetRandomFreeSide(); + TileSide side = branch.GetRandomFreeSide(); + if (side == TileSide.None) { return availableBranches; } - if (side == TileSide.None) { continue; } + Behavior.TryGrowBranch(branch, side, out List result); + availableBranches.Clear(); + availableBranches.Add(branch); - Behavior.TryGrowBranch(branch, side, out List result); - newBranches.AddRange(result); - } - - return newBranches; + return availableBranches; } private Item? ScanForTargets(VineTile branch) @@ -117,22 +117,22 @@ namespace Barotrauma.MapCreatures.Behavior int highestPriority = 0; Item? currentItem = null; - foreach (Item item in Item.ItemList.Where(it => !Behavior.ClaimedTargets.Contains(it))) + foreach (Item item in Item.ItemList) { + if (item.Submarine != parent.Submarine || Vector2.DistanceSquared(worldPos, item.WorldPosition) > Behavior.Sight * Behavior.Sight) { continue; } + if (Behavior.ClaimedTargets.Contains(item)) { continue; } if (Behavior.IgnoredTargets.ContainsKey(item)) { continue; } int priority = 0; foreach (BallastFloraBehavior.AITarget target in Behavior.Targets) { - if (!target.Matches(item) || target.Priority <= highestPriority) { continue; } + if (target.Priority <= highestPriority || !target.Matches(item)) { continue; } priority = target.Priority; break; } if (priority == 0) { continue; } - if (item.Submarine != parent.Submarine || Vector2.Distance(worldPos, item.WorldPosition) > Behavior.Sight) { continue; } - Vector2 itemSimPos = ConvertUnits.ToSimUnits(item.Position); #if DEBUG @@ -156,6 +156,7 @@ namespace Barotrauma.MapCreatures.Behavior { foreach (BallastFloraBranch existingBranch in Behavior.Branches) { + if (existingBranch.Health <= 0 || existingBranch.IsRootGrowth) { continue; } if (Behavior.BranchContainsTarget(existingBranch, currentItem)) { Behavior.ClaimTarget(currentItem, existingBranch); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs index 4d30f0f94..82254622b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs @@ -53,7 +53,7 @@ namespace Barotrauma.MapCreatures.Behavior List newList = new List(TargetBranches); foreach (BallastFloraBranch branch in newList) { - if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; } + if (branch.FailedGrowthAttempts > 8 || branch.DisconnectedFromRoot || !branch.CanGrowMore()) { continue; } // Get what side gets us closest to the target TileSide side = GetClosestSide(branch, Target.WorldPosition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 4c356b657..ab74f2a8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; @@ -70,6 +71,14 @@ namespace Barotrauma public double SpawnTime => spawnTime; private readonly double spawnTime; + private static UInt64 creationCounter = 0; + private readonly static object creationCounterMutex = new object(); + + public readonly string CreationStackTrace; + public readonly UInt64 CreationIndex; + public string ErrorLine + => $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}"; + public Entity(Submarine submarine, ushort id) { this.Submarine = submarine; @@ -84,6 +93,31 @@ namespace Barotrauma } dictionary.Add(ID, this); + + CreationStackTrace = ""; +#if DEBUG + var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true); + var frames = st.GetFrames(); + int frameCount = 0; + foreach (var frame in frames) + { + string fileName = frame.GetFileName(); + if ((fileName?.Contains("BarotraumaClient") ?? false) || (fileName?.Contains("BarotraumaServer") ?? false)) { break; } + + fileName = Path.GetFileNameWithoutExtension(fileName); + int fileLineNumber = frame.GetFileLineNumber(); + + if (fileName.IsNullOrEmpty() || fileLineNumber <= 0) { continue; } + + CreationStackTrace += $"{fileName}@{fileLineNumber}; "; + } +#endif + #warning TODO: consider removing this mutex, entity creation probably shouldn't be multithreaded + lock (creationCounterMutex) + { + CreationIndex = creationCounter; + creationCounter++; + } } protected virtual ushort DetermineID(ushort id, Submarine submarine) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 1c0e18576..442917563 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -59,10 +59,10 @@ namespace Barotrauma smoke = true; flames = true; underwaterBubble = true; - ignoreFireEffectsForTags = new string[0]; + ignoreFireEffectsForTags = Array.Empty(); } - public Explosion(XElement element, string parentDebugName) + public Explosion(ContentXElement element, string parentDebugName) { Attack = new Attack(element, parentDebugName + ", Explosion"); @@ -80,7 +80,7 @@ namespace Barotrauma playTinnitus = element.GetAttributeBool("playtinnitus", showEffects); applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects); - ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", new string[0], convertToLowerInvariant: true); + ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty(), convertToLowerInvariant: true); ignoreCover = element.GetAttributeBool("ignorecover", false); onlyInside = element.GetAttributeBool("onlyinside", false); @@ -482,7 +482,7 @@ namespace Barotrauma { List ballastFlorae = new List(); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.BallastFlora != null) { ballastFlorae.Add(hull.BallastFlora); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 3840832c6..bc8844700 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -136,7 +136,7 @@ namespace Barotrauma { } public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID) - : base(MapEntityPrefab.Find(null, "gap"), submarine, id) + : base(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), submarine, id) { this.rect = rect; flowForce = Vector2.Zero; @@ -706,7 +706,7 @@ namespace Barotrauma base.ShallowRemove(); GapList.Remove(this); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); } @@ -717,7 +717,7 @@ namespace Barotrauma base.Remove(); GapList.Remove(this); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); } @@ -734,7 +734,7 @@ namespace Barotrauma if (!DisableHullRechecks) FindHulls(); } - public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = Rectangle.Empty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 869d1fe35..d86671e8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -101,8 +101,8 @@ namespace Barotrauma partial class Hull : MapEntity, ISerializableEntity, IServerSerializable { - public static List hullList = new List(); - public static List EntityGrids { get; } = new List(); + public readonly static List HullList = new List(); + public readonly static List EntityGrids = new List(); public static bool ShowHulls = true; @@ -124,8 +124,8 @@ namespace Barotrauma public const int BackgroundSectionsPerNetworkEvent = 16; - public readonly Dictionary properties; - public Dictionary SerializableProperties + public readonly Dictionary properties; + public Dictionary SerializableProperties { get { return properties; } } @@ -155,32 +155,23 @@ namespace Barotrauma public readonly List ConnectedGaps = new List(); - public override string Name - { - get - { - return "Hull"; - } - } + public override string Name => "Hull"; - public string DisplayName + public LocalizedString DisplayName { get; private set; } - private readonly HashSet moduleTags = new HashSet(); + private readonly HashSet moduleTags = new HashSet(); /// /// Inherited flags from outpost generation. /// - public IEnumerable OutpostModuleTags - { - get { return moduleTags; } - } + public IEnumerable OutpostModuleTags => moduleTags; private string roomName; - [Editable, Serialize("", true, translationTextTag: "RoomName.")] + [Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "RoomName.")] public string RoomName { get { return roomName; } @@ -188,7 +179,7 @@ namespace Barotrauma { if (roomName == value) { return; } roomName = value; - DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName; + DisplayName = TextManager.Get(roomName).Fallback(roomName); if (!IsWetRoom && ForceAsWetRoom) { IsWetRoom = true; @@ -200,7 +191,7 @@ namespace Barotrauma private Color ambientLight; - [Editable, Serialize("0,0,0,0", true)] + [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes)] public Color AmbientLight { get { return ambientLight; } @@ -314,7 +305,7 @@ namespace Barotrauma } } - [Serialize(100000.0f, true)] + [Serialize(100000.0f, IsPropertySaveable.Yes)] public float Oxygen { get { return oxygen; } @@ -332,7 +323,7 @@ namespace Barotrauma roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)); private bool isWetRoom; - [Editable, Serialize(false, true, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")] public bool IsWetRoom { get { return isWetRoom; } @@ -347,7 +338,7 @@ namespace Barotrauma } private bool avoidStaying; - [Editable, Serialize(false, true, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")] public bool AvoidStaying { get { return avoidStaying || IsWetRoom; } @@ -469,7 +460,7 @@ namespace Barotrauma }; } - hullList.Add(this); + HullList.Add(this); if (submarine == null || !submarine.Loading) { @@ -488,11 +479,11 @@ namespace Barotrauma public static Rectangle GetBorders() { - if (!hullList.Any()) return Rectangle.Empty; + if (!HullList.Any()) return Rectangle.Empty; - Rectangle rect = hullList[0].rect; + Rectangle rect = HullList[0].rect; - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { if (hull.Rect.X < rect.X) { @@ -516,12 +507,15 @@ namespace Barotrauma public override MapEntity Clone() { - var clone = new Hull(MapEntityPrefab.Find(null, "hull"), rect, Submarine); - foreach (KeyValuePair property in SerializableProperties) + var clone = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), rect, Submarine); + foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) { continue; } clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); } +#if CLIENT + clone.lastAmbientLightEditTime = 0.0; +#endif return clone; } @@ -536,17 +530,17 @@ namespace Barotrauma { var newGrid = new EntityGrid(submarine, 200.0f); EntityGrids.Add(newGrid); - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { if (hull.Submarine == submarine && !hull.IdFreed) { newGrid.InsertEntity(hull); } } return newGrid; } - public void SetModuleTags(IEnumerable tags) + public void SetModuleTags(IEnumerable tags) { moduleTags.Clear(); - foreach (string tag in tags) + foreach (Identifier tag in tags) { moduleTags.Add(tag); } @@ -630,7 +624,7 @@ namespace Barotrauma public override void ShallowRemove() { base.Remove(); - hullList.Remove(this); + HullList.Remove(this); if (Submarine == null || (!Submarine.Loading && !Submarine.Unloading)) { @@ -659,7 +653,7 @@ namespace Barotrauma public override void Remove() { base.Remove(); - hullList.Remove(this); + HullList.Remove(this); BallastFlora?.Remove(); if (Submarine != null && !Submarine.Loading && !Submarine.Unloading) @@ -712,7 +706,7 @@ namespace Barotrauma return null; } - var decal = GameMain.DecalManager.Prefabs.Find(p => p.UIntIdentifier == decalId); + var decal = DecalManager.Prefabs.Find(p => p.UintIdentifier == decalId); if (decal == null) { DebugConsole.ThrowError($"Could not find a decal prefab with the UInt identifier {decalId}!"); @@ -732,7 +726,7 @@ namespace Barotrauma if (decals.Count >= MaxDecalsPerHull) { return null; } - var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex); + var decal = DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex); if (decal != null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -1110,12 +1104,12 @@ namespace Barotrauma /// public static Hull FindHullUnoptimized(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true) { - if (guess != null && hullList.Contains(guess)) + if (guess != null && HullList.Contains(guess)) { if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess; } - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull; } @@ -1135,15 +1129,15 @@ namespace Barotrauma else { Hull h = c.CurrentHull; - hullList.ForEach(j => j.Visible = false); + HullList.ForEach(j => j.Visible = false); List visibleHulls; if (h == null || c.Submarine == null) { - visibleHulls = hullList.FindAll(j => j.CanSeeOther(null, false)); + visibleHulls = HullList.FindAll(j => j.CanSeeOther(null, false)); } else { - visibleHulls = hullList.FindAll(j => h.CanSeeOther(j, true)); + visibleHulls = HullList.FindAll(j => h.CanSeeOther(j, true)); } visibleHulls.ForEach(j => j.Visible = true); foreach (Item it in Item.ItemList) @@ -1164,7 +1158,7 @@ namespace Barotrauma foreach (Gap g in ConnectedGaps) { if (g.ConnectedWall != null && g.ConnectedWall.CastShadow) continue; - List otherHulls = hullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this); + List otherHulls = HullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this); retVal = otherHulls.Any(h => h == other); if (!retVal && allowIndirect) retVal = otherHulls.Any(h => h.CanSeeOther(other, false)); if (retVal) return true; @@ -1174,7 +1168,7 @@ namespace Barotrauma { foreach (Gap g in ConnectedGaps) { - if (g.ConnectedDoor != null && !hullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true; + if (g.ConnectedDoor != null && !HullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true; } List structures = mapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect)); return structures.Any(st => !(st as Structure).CastShadow); @@ -1209,7 +1203,7 @@ namespace Barotrauma if (moduleFlags != null && moduleFlags.Any() && (Submarine.Info.Type == SubmarineType.OutpostModule || Submarine.Info.Type == SubmarineType.Outpost)) { - if (moduleFlags.Contains("airlock") && + if (moduleFlags.Contains("airlock".ToIdentifier()) && ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null)) { return "RoomName.Airlock"; @@ -1313,7 +1307,7 @@ namespace Barotrauma public static Hull GetCleanTarget(Vector2 worldPosition) { - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { Rectangle worldRect = hull.WorldRect; if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) { continue; } @@ -1476,7 +1470,7 @@ namespace Barotrauma } #endregion - public static Hull Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect; if (element.Attribute("rect") != null) @@ -1507,7 +1501,7 @@ namespace Barotrauma hull.OriginalAmbientLight = XMLExtensions.ParseColor(originalAmbientLight, false); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1525,7 +1519,7 @@ namespace Barotrauma } break; case "ballastflorabehavior": - string identifier = subElement.GetAttributeString("identifier", string.Empty); + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); BallastFloraPrefab prefab = BallastFloraPrefab.Find(identifier); if (prefab != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 5793f5b9c..13d72bc6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -5,64 +5,57 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; +using System.Net; namespace Barotrauma { partial class ItemAssemblyPrefab : MapEntityPrefab { - private readonly string name; - public override string Name { get { return name; } } - public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies"); public static readonly string SaveFolder = "ItemAssemblies"; - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } - private readonly XElement configElement; - - public List> DisplayEntities + + public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities; + + public readonly Rectangle Bounds; + + public override LocalizedString Name { get; } + + public override Sprite Sprite => null; + + public override string OriginalName => Name.Value; + + public override ImmutableHashSet Tags { get; } + + public override ImmutableHashSet AllowedLinks => null; + + public override MapEntityCategory Category => MapEntityCategory.ItemAssembly; + + public override ImmutableHashSet Aliases => null; + + protected override Identifier DetermineIdentifier(XElement element) { - get; - private set; + return element.GetAttributeIdentifier("identifier", element.GetAttributeIdentifier("name", "")); } - public Rectangle Bounds; - - public ItemAssemblyPrefab(string filePath, bool allowOverwrite = false) + public ItemAssemblyPrefab(ContentXElement element, ItemAssemblyFile file) : base(element, file) { - FilePath = filePath; - XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null) { return; } - - XElement element = doc.Root; - if (element.IsOverride()) - { - element = element.Elements().First(); - } - - originalName = element.GetAttributeString("name", ""); - identifier = element.GetAttributeString("identifier", null) ?? originalName.ToLowerInvariant().Replace(" ", ""); configElement = element; - Category = MapEntityCategory.ItemAssembly; - SerializableProperty.DeserializeProperties(this, configElement); - name = TextManager.Get("EntityName." + identifier, returnNull: true) ?? originalName; - Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description; + Name = TextManager.Get($"EntityName.{Identifier}").Fallback(element.GetAttributeString("name", "")); + Description = TextManager.Get($"EntityDescription.{Identifier}"); + Tags = Enumerable.Empty().ToImmutableHashSet(); List containedItemIDs = new List(); foreach (XElement entityElement in element.Elements()) { - var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase)); + var containerElement = entityElement.GetChildElement("itemcontainer"); if (containerElement == null) { continue; } string containedString = containerElement.GetAttributeString("contained", ""); @@ -84,52 +77,36 @@ namespace Barotrauma int minX = int.MaxValue, minY = int.MaxValue; int maxX = int.MinValue, maxY = int.MinValue; - DisplayEntities = new List>(); + var displayEntities = new List<(Identifier, Rectangle)>(); foreach (XElement entityElement in element.Elements()) { ushort id = (ushort)entityElement.GetAttributeInt("ID", 0); if (id > 0 && containedItemIDs.Contains(id)) { continue; } - string identifier = entityElement.GetAttributeString("identifier", entityElement.Name.ToString().ToLowerInvariant()); - MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier); - if (mapEntity == null) - { - string entityName = entityElement.GetAttributeString("name", ""); - mapEntity = List.FirstOrDefault(p => p.Name == entityName); - } + Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant()); Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty); - if (mapEntity != null && !entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) + if (!entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) { - if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { DisplayEntities.Add(new Pair(mapEntity, rect)); } + if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { displayEntities.Add((identifier, rect)); } minX = Math.Min(minX, rect.X); minY = Math.Min(minY, rect.Y - rect.Height); maxX = Math.Max(maxX, rect.Right); maxY = Math.Max(maxY, rect.Y); } } + DisplayEntities = displayEntities.ToImmutableArray(); Bounds = minX == int.MaxValue ? new Rectangle(0, 0, 1, 1) : new Rectangle(minX, minY, maxX - minX, maxY - minY); - - if (allowOverwrite && Prefabs.ContainsKey(identifier)) - { - Prefabs.Remove(Prefabs[identifier]); - } - Prefabs.Add(this, doc.Root.IsOverride()); } - public static void Remove(string filePath) - { - Prefabs.RemoveByFile(filePath); - } - protected override void CreateInstance(Rectangle rect) { #if CLIENT var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub, selectInstance: Screen.Selected == GameMain.SubEditorScreen); - if (Screen.Selected is SubEditorScreen) + if (Screen.Selected is SubEditorScreen && loaded.Any()) { SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false)); } @@ -140,7 +117,7 @@ namespace Barotrauma public List CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false) { - return PasteEntities(position, sub, configElement, FilePath, selectInstance); + return PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance); } public static List PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false) @@ -183,53 +160,19 @@ namespace Barotrauma public void Delete() { Dispose(); - if (File.Exists(FilePath)) + if (File.Exists(ContentFile.Path)) { try { - File.Delete(FilePath); + File.Delete(ContentFile.Path); } catch (Exception e) { - DebugConsole.ThrowError("Deleting item assembly \"" + name + "\" failed.", e); + DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); } } } - public static void LoadAll() - { - if (GameSettings.VerboseLogging) - { - DebugConsole.Log("Loading item assembly prefabs: "); - } - - List itemAssemblyFiles = new List(); - - //find assembly files in the item assembly folders - if (Directory.Exists(VanillaSaveFolder)) - { - itemAssemblyFiles.AddRange(Directory.GetFiles(VanillaSaveFolder)); - } - if (Directory.Exists(SaveFolder)) - { - itemAssemblyFiles.AddRange(Directory.GetFiles(SaveFolder)); - } - - //find assembly files in selected content packages - foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) - { - foreach (string filePath in cp.GetFilesOfType(ContentType.ItemAssembly)) - { - //ignore files that have already been added (= file saved to item assembly folder) - if (itemAssemblyFiles.Any(f => Path.GetFullPath(f) == Path.GetFullPath(filePath))) { continue; } - itemAssemblyFiles.Add(filePath); - } - } - - foreach (string file in itemAssemblyFiles) - { - new ItemAssemblyPrefab(file); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs new file mode 100644 index 000000000..1205fa136 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Xml.Linq; + +namespace Barotrauma +{ + class Biome : PrefabWithUintIdentifier + { + public readonly static PrefabCollection Prefabs = new PrefabCollection(); + + public readonly Identifier OldIdentifier; + public readonly LocalizedString DisplayName; + public readonly LocalizedString Description; + + public readonly bool IsEndBiome; + + public readonly ImmutableHashSet AllowedZones; + + public Biome(ContentXElement element, LevelGenerationParametersFile file) : base(file, ParseIdentifier(element)) + { + OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty); + + DisplayName = + TextManager.Get("biomename." + Identifier).Fallback( + element.GetAttributeString("name", "Biome")); + + Description = + TextManager.Get("biomedescription." + Identifier).Fallback( + element.GetAttributeString("description", "")); + + IsEndBiome = element.GetAttributeBool("endbiome", false); + + AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); + } + + public static Identifier ParseIdentifier(ContentXElement element) + { + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + if (identifier.IsEmpty) + { + identifier = element.GetAttributeIdentifier("name", ""); + DebugConsole.ThrowError("Error in biome \"" + identifier + "\": identifier missing, using name as the identifier."); + } + return identifier; + } + + public override void Dispose() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index e365e2c66..4adb2834f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -7,23 +7,18 @@ using System.Xml.Linq; namespace Barotrauma { - class CaveGenerationParams : ISerializableEntity + class CaveGenerationParams : PrefabWithUintIdentifier, ISerializableEntity { - public static List CaveParams { get; private set; } + public readonly static PrefabCollection CaveParams = new PrefabCollection(); - public string Name - { - get { return Identifier; } - } - - public readonly string Identifier; + public string Name => Identifier.Value; private int minWidth, maxWidth; private int minHeight, maxHeight; private int minBranchCount, maxBranchCount; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; @@ -33,100 +28,100 @@ namespace Barotrauma /// Overrides the commonness of the object in a specific level type. /// Key = name of the level type, value = commonness in that level type. /// - public readonly Dictionary OverrideCommonness = new Dictionary(); + public readonly Dictionary OverrideCommonness = new Dictionary(); - [Editable, Serialize(1.0f, true)] + [Editable, Serialize(1.0f, IsPropertySaveable.Yes)] public float Commonness { get; private set; } - [Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)] + [Serialize(8000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 100000)] public int MinWidth { get { return minWidth; } set { minWidth = Math.Max(value, 1000); } } - [Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] + [Serialize(10000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] public int MaxWidth { get { return maxWidth; } set { maxWidth = Math.Max(value, minWidth); } } - [Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)] + [Serialize(8000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 100000)] public int MinHeight { get { return minHeight; } set { minHeight = Math.Max(value, 1000); } } - [Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] + [Serialize(10000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] public int MaxHeight { get { return maxHeight; } set { maxHeight = Math.Max(value, minHeight); } } - [Serialize(2, true), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(2, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinBranchCount { get { return minBranchCount; } set { minBranchCount = Math.Max(value, 0); } } - [Serialize(4, true), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(4, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxBranchCount { get { return maxBranchCount; } set { maxBranchCount = Math.Max(value, minBranchCount); } } - [Serialize(50, true), Editable(MinValueInt = 0, MaxValueInt = 1000)] + [Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)] public int LevelObjectAmount { get; set; } - [Serialize(0.1f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] + [Serialize(0.1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] public float DestructibleWallRatio { get; set; } - public Sprite WallSprite { get; private set; } - public Sprite WallEdgeSprite { get; private set; } + public readonly Sprite WallSprite; + public readonly Sprite WallEdgeSprite; public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand) { - if (CaveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) + var caveParams = CaveParams.OrderBy(p => p.UintIdentifier).ToList(); + if (caveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) { - return CaveParams.First(); + return caveParams.First(); } - return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); + return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); } public float GetCommonness(LevelGenerationParams generationParams, bool abyss) { - if (generationParams?.Identifier != null && - OverrideCommonness.TryGetValue(abyss ? "abyss" : generationParams.Identifier, out float commonness)) + if (generationParams != null && + generationParams.Identifier != Identifier.Empty && + OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : generationParams.Identifier, out float commonness)) { return commonness; } return Commonness; } - private CaveGenerationParams(XElement element) + public CaveGenerationParams(ContentXElement element, CaveGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - Identifier = element == null ? "default" : element.GetAttributeString("identifier", null) ?? element.Name.ToString(); - Identifier = Identifier.ToLowerInvariant(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -137,7 +132,7 @@ namespace Barotrauma WallEdgeSprite = new Sprite(subElement); break; case "overridecommonness": - string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelType = subElement.GetAttributeIdentifier("leveltype", ""); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); @@ -147,71 +142,16 @@ namespace Barotrauma } } - public static void LoadPresets() - { - CaveParams = new List(); - - var files = GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters); - if (!files.Any()) - { - files = new List() { new ContentFile("Content/Map/CaveGenerationParameters.xml", ContentType.CaveGenerationParameters) }; - } - - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - CaveParams.Clear(); - DebugConsole.NewMessage($"Overriding cave generation parameters with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - if (isOverride) - { - string identifier = element.FirstElement().GetAttributeString("identifier", ""); - var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (existingParams != null) - { - DebugConsole.NewMessage($"Overriding the cave generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow); - CaveParams.Remove(existingParams); - } - CaveParams.Add(new CaveGenerationParams(element.FirstElement())); - - } - else - { - string identifier = element.FirstElement().GetAttributeString("identifier", ""); - var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (existingParams != null) - { - DebugConsole.ThrowError($"Duplicate cave generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use tags to override the generation parameters."); - continue; - } - else - { - CaveParams.Add(new CaveGenerationParams(element)); - } - } - } - } - } - public void Save(XElement element) { SerializableProperty.SerializeProperties(this, element, true); - foreach (KeyValuePair overrideCommonness in OverrideCommonness) + foreach (KeyValuePair overrideCommonness in OverrideCommonness) { bool elementFound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - if (subElement.Name.ToString().Equals("overridecommonness", StringComparison.OrdinalIgnoreCase) - && subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, StringComparison.OrdinalIgnoreCase)) + if (subElement.NameAsIdentifier() == "overridecommonness" + && subElement.GetAttributeIdentifier("leveltype", "") == overrideCommonness.Key) { subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); elementFound = true; @@ -226,5 +166,10 @@ namespace Barotrauma } } } + + public override void Dispose() + { + WallSprite?.Remove(); WallEdgeSprite?.Remove(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 8d1c11c4b..735066d2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -279,7 +279,7 @@ namespace Barotrauma { float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount)); - float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server); + float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient); Vector2 extrudedPoint = edge.Point1 + edgeDir * (i / (float)pointCount) + @@ -469,7 +469,7 @@ namespace Barotrauma for (int i = 0; i < vertexCount; i++) { Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); - verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)); + verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.ServerAndClient)); angle += angleStep; } return verts; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index bd22ea0ce..5fac8f4b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -11,6 +11,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.IO; using Voronoi2; namespace Barotrauma @@ -400,6 +401,8 @@ namespace Barotrauma Loaded = this; Generating = true; + Rand.Tracker.Reset(); + Rand.Tracker.Active = true; EqualityCheckValues.Clear(); EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); @@ -410,7 +413,7 @@ namespace Barotrauma EndLocation = GameMain.GameSession?.EndLocation; } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); LevelObjectManager = new LevelObjectManager(); @@ -420,11 +423,8 @@ namespace Barotrauma #if CLIENT if (backgroundCreatureManager == null) { - var files = GameMain.Instance.GetFilesOfType(ContentType.BackgroundCreaturePrefabs); - if (files.Count() > 0) - backgroundCreatureManager = new BackgroundCreatureManager(files); - else - backgroundCreatureManager = new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml"); + var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).ToArray(); + backgroundCreatureManager = files.Any() ? new BackgroundCreatureManager(files) : new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml"); } #endif Stopwatch sw = new Stopwatch(); @@ -476,7 +476,7 @@ namespace Barotrauma (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y)); endExitPosition = new Point(endPosition.X, borders.Bottom); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- //generate the initial nodes for the main path and smaller tunnels @@ -550,20 +550,20 @@ namespace Barotrauma Tunnels.Add(abyssTunnel); } - int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.Server); + int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.ServerAndClient); for (int j = 0; j < sideTunnelCount; j++) { if (mainPath.Nodes.Count < 4) { break; } var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel); - Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.Server)]; + Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)]; if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } - Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.Server)]; - Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.Server)]; + Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.ServerAndClient)]; + Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.ServerAndClient)]; var sidePathNodes = GeneratePathNodes(branchStart, branchEnd, pathBorders, tunnelToBranchOff, GenerationParams.SideTunnelVariance); //make sure the path is wide enough to pass through - int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.Server); + int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.ServerAndClient); Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff)); } @@ -572,7 +572,7 @@ namespace Barotrauma GenerateAbyssArea(); GenerateCaves(mainPath); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- //generate voronoi sites @@ -583,21 +583,21 @@ namespace Barotrauma Point siteVariance = GenerationParams.VoronoiSiteVariance; siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); - int caveSiteInterval = 500; + const int caveSiteInterval = 500; for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) { for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) { - int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.Server); - int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.Server); + int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient); + int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient); bool closeToTunnel = false; bool closeToCave = false; foreach (Tunnel tunnel in Tunnels) { + float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); for (int i = 1; i < tunnel.Nodes.Count; i++) { - float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; } if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; } if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; } @@ -607,7 +607,7 @@ namespace Barotrauma if (Math.Sqrt(tunnelDistSqr) < minDist) { closeToTunnel = true; - tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + //tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); if (tunnel.Type == TunnelType.Cave) { closeToCave = true; @@ -620,7 +620,7 @@ namespace Barotrauma if (!closeToTunnel) { //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry - if (Rand.Range(0, 10, Rand.RandSync.Server) != 0) { continue; } + if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } } if (!TooClose(siteX, siteY)) @@ -635,8 +635,8 @@ namespace Barotrauma { for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) { - int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); - int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); + int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); + int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); if (!TooClose(caveSiteX, caveSiteY)) { @@ -677,7 +677,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // construct the voronoi graph and cells @@ -778,7 +778,7 @@ namespace Barotrauma for (int i = 0; i < GenerationParams.IslandCount; i++) { if (potentialIslands.Count == 0) { break; } - var island = potentialIslands.GetRandom(Rand.RandSync.Server); + var island = potentialIslands.GetRandom(Rand.RandSync.ServerAndClient); island.CellType = CellType.Solid; island.Island = true; pathCells.Remove(island); @@ -795,7 +795,7 @@ namespace Barotrauma startPosition.X = (int)pathCells[0].Site.Coord.X; startExitPosition.X = startPosition.X; - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level @@ -862,8 +862,6 @@ namespace Barotrauma // mirror if needed //---------------------------------------------------------------------------------- - int asdfasdf = Rand.Int(int.MaxValue, Rand.RandSync.Server); - if (mirror) { HashSet mirroredEdges = new HashSet(); @@ -1008,7 +1006,7 @@ namespace Barotrauma caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells)); foreach (var caveCell in caveCells) { - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio) { var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f); if (chunk != null) @@ -1020,7 +1018,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // create some ruins @@ -1033,7 +1031,7 @@ namespace Barotrauma GenerateRuin(ruinPositions[i], mirror); } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // create floating ice chunks @@ -1054,18 +1052,18 @@ namespace Barotrauma for (int i = 0; i < GenerationParams.FloatingIceChunkCount; i++) { if (iceChunkPositions.Count == 0) { break; } - Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.Server)]; - float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.Server); + Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.ServerAndClient)]; + float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.ServerAndClient); var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f); var chunk = CreateIceChunk(vertices, selectedPos.ToVector2()); chunk.MoveAmount = new Vector2(0.0f, minMainPathWidth * 0.7f); - chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server); + chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.ServerAndClient); ExtraWalls.Add(chunk); iceChunkPositions.Remove(selectedPos); } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // generate the bodies and rendered triangles of the cells @@ -1170,7 +1168,7 @@ namespace Barotrauma } #endif - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // create ice spires @@ -1205,7 +1203,7 @@ namespace Barotrauma CreateOutposts(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // top barrier & sea floor @@ -1247,15 +1245,15 @@ namespace Barotrauma CreateWrecks(); CreateBeaconStation(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); GenerateItems(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); #if CLIENT backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount); @@ -1283,7 +1281,7 @@ namespace Barotrauma Debug.WriteLine("Seed: " + Seed); Debug.WriteLine("**********************************************************************************"); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Generated level with the seed " + Seed + " (type: " + GenerationParams.Identifier + ")", Color.White); } @@ -1301,6 +1299,8 @@ namespace Barotrauma //assign an ID to make entity events work //ID = FindFreeID(); Generating = false; + Rand.Tracker.Active = false; + File.WriteAllLines(GameMain.NetworkMember is { IsServer: true } ? "serverrng.txt" : "clientrng.txt", Rand.Tracker.LogMsgs); } private List GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance) @@ -1311,9 +1311,9 @@ namespace Barotrauma for (int x = startPosition.X + nodeInterval.X; x < endPosition.X - nodeInterval.X; - x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server)) + x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.ServerAndClient)) { - Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server)); + Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.ServerAndClient)); //allow placing the 2nd main path node at any height regardless of variance //(otherwise low variance will always make the main path go through the upper part of the level) @@ -1378,7 +1378,7 @@ namespace Barotrauma foreach (VoronoiCell cell in cells) { if (cell.Edges.Any(e => e.NextToCave)) { continue; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > holeProbability) { continue; } + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > holeProbability) { continue; } if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) { continue; } float closestDist = 0.0f; @@ -1620,13 +1620,13 @@ namespace Barotrauma //above the bottom of the level = can't place a point here if (seaFloorPos > AbyssStart) { continue; } - float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.Server)); + float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.ServerAndClient)); foreach (var abyssIsland in AbyssIslands) { if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos))) { - xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.Server) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f); + xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.ServerAndClient) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f); } } @@ -1681,7 +1681,7 @@ namespace Barotrauma Point islandSize = Vector2.Lerp( GenerationParams.AbyssIslandSizeMin.ToVector2(), GenerationParams.AbyssIslandSizeMax.ToVector2(), - Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)).ToPoint(); + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)).ToPoint(); if (AbyssArea.Height < islandSize.Y) { return; } @@ -1697,8 +1697,8 @@ namespace Barotrauma do { islandPosition = new Point( - Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.Server), - Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.Server)); + Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.ServerAndClient), + Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.ServerAndClient)); //move the island above the sea floor geometry islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X).Y + 500); @@ -1714,7 +1714,7 @@ namespace Barotrauma break; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > GenerationParams.AbyssIslandCaveProbability) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability) { float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); @@ -1735,8 +1735,8 @@ namespace Barotrauma { for (int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y) { - siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server)); - siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server)); + siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.ServerAndClient)); + siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.ServerAndClient)); } } @@ -1765,7 +1765,7 @@ namespace Barotrauma } } - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.Server); + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.ServerAndClient); float caveScaleRelativeToIsland = 0.7f; GenerateCave( @@ -1786,12 +1786,12 @@ namespace Barotrauma new Point(0, BottomPos) }; - int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.Server); + int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.ServerAndClient); for (int i = 0; i < mountainCount; i++) { bottomPositions.Add( new Point(Size.X / (mountainCount + 1) * (i + 1), - BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.Server))); + BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.ServerAndClient))); } bottomPositions.Add(new Point(Size.X, BottomPos)); @@ -1804,8 +1804,8 @@ namespace Barotrauma bottomPositions.Insert(i + 1, new Point( (bottomPositions[i].X + bottomPositions[i + 1].X) / 2, - (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.Server))); - i++; + (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.ServerAndClient))); + i++; } currInverval /= 2; @@ -1834,10 +1834,10 @@ namespace Barotrauma { for (int i = 0; i < GenerationParams.CaveCount; i++) { - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.Server); + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.ServerAndClient); Point caveSize = new Point( - Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.Server), - Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.Server)); + Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient), + Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient)); int padding = (int)(caveSize.X * 1.2f); Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2); @@ -1891,12 +1891,12 @@ namespace Barotrauma Tunnels.Add(tunnel); caveBranches.Add(tunnel); - int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.Server); + int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.ServerAndClient); for (int j = 0; j < branches; j++) { - Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); - Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.Server)].ToVector2(); - Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.Server)].ToVector2(); + Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.ServerAndClient); + Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.ServerAndClient)].ToVector2(); + Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.ServerAndClient)].ToVector2(); var branchSegments = MathUtils.GenerateJaggedLine( branchStartPos, branchEndPos, iterations: 3, @@ -1930,17 +1930,17 @@ namespace Barotrauma private void GenerateRuin(Point ruinPos, bool mirror) { - var ruinGenerationParams = RuinGenerationParams.GetRandom(Rand.RandSync.Server); + var ruinGenerationParams = RuinGenerationParams.RuinParams.GetRandom(Rand.RandSync.ServerAndClient); LocationType locationType = StartLocation?.Type; if (locationType == null) { - locationType = LocationType.List.GetRandom(Rand.RandSync.Server); + locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); if (ruinGenerationParams.AllowedLocationTypes.Any()) { - locationType = LocationType.List.Where(lt => + locationType = LocationType.Prefabs.Where(lt => ruinGenerationParams.AllowedLocationTypes.Any(allowedType => - allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server); + allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient); } } @@ -2111,7 +2111,7 @@ namespace Barotrauma if (pointsAboveBottom.Count == 0) { DebugConsole.ThrowError("Error in FindPosAwayFromMainPath: no valid positions above the bottom of the sea floor. Has the position of the sea floor been set too high up?"); - return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.Server)].point; + return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.ServerAndClient)].point; } var validPoints = pointsAboveBottom.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point))); @@ -2154,7 +2154,7 @@ namespace Barotrauma } else { - return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].point; + return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.ServerAndClient)].point; } } @@ -2270,7 +2270,7 @@ namespace Barotrauma { const float maxLength = 15000.0f; float minEdgeLength = 100.0f; - var mainPathPos = PositionsOfInterest.Where(pos => pos.PositionType == PositionType.MainPath).GetRandom(Rand.RandSync.Server); + var mainPathPos = PositionsOfInterest.GetRandom(pos => pos.PositionType == PositionType.MainPath, Rand.RandSync.ServerAndClient); double closestDistSqr = double.PositiveInfinity; GraphEdge closestEdge = null; VoronoiCell closestCell = null; @@ -2307,13 +2307,13 @@ namespace Barotrauma float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength); spireLength *= MathHelper.Lerp(0.3f, 1.5f, Difficulty / 100.0f); - Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); - Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); + Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient); + Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient); List vertices = new List() { closestEdge.Point1, - extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server), - extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server), + extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient), + extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient), closestEdge.Point2, }; Vector2 center = Vector2.Zero; @@ -2364,8 +2364,8 @@ namespace Barotrauma }; } } - public List ResourceTags { get; } - public List ResourceIds { get; } + public List ResourceTags { get; } + public List ResourceIds { get; } public List ClusterLocations { get; } public TunnelType TunnelType { get; } @@ -2374,8 +2374,8 @@ namespace Barotrauma Id = id; Position = position; ShouldContainResources = shouldContainResources; - ResourceTags = new List(); - ResourceIds = new List(); + ResourceTags = new List(); + ResourceIds = new List(); ClusterLocations = new List(); TunnelType = tunnelType; } @@ -2417,14 +2417,14 @@ namespace Barotrauma // Such as the exploding crystals in The Great Sea private void GenerateItems() { - string levelName = GenerationParams.Identifier.ToLowerInvariant(); + Identifier levelName = GenerationParams.Identifier; float minCommonness = float.MaxValue, maxCommonness = float.MinValue; List<(ItemPrefab itemPrefab, float commonness)> levelResources = new List<(ItemPrefab itemPrefab, float commonness)>(); var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>(); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier)) { if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || - itemPrefab.LevelCommonness.TryGetValue("", out commonness)) + itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness)) { if (commonness <= 0.0f) { continue; } if (commonness < minCommonness) { minCommonness = commonness; } @@ -2432,7 +2432,7 @@ namespace Barotrauma levelResources.Add((itemPrefab, commonness)); } else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) || - itemPrefab.LevelQuantity.TryGetValue("", out fixedQuantityResourceInfo)) + itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo)) { fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); } @@ -2450,12 +2450,12 @@ namespace Barotrauma var location = allValidLocations.GetRandom(l => { if (l.Cell == null || l.Edge == null) { return false; } - if (resourceInfo.IsIslandSpecifc && !l.Cell.Island) { return false; } + if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; } if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; } if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; } return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _); - }, randSync: Rand.RandSync.Server); + }, randSync: Rand.RandSync.ServerAndClient); if (location.Cell == null || location.Edge == null) { break; } @@ -2478,10 +2478,10 @@ namespace Barotrauma if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } l.InitializeResources(); return l.Resources.Count <= GetMaxResourcesOnEdge(itemPrefab, l, out _); - }, randSync: Rand.RandSync.Server); + }, randSync: Rand.RandSync.ServerAndClient); if (location.Cell == null || location.Edge == null) { break; } - int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.Server); + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient); PlaceResources(itemPrefab, clusterSize, location, out var abyssResources); var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); abyssClusterLocation.Resources.AddRange(abyssResources); @@ -2509,7 +2509,7 @@ namespace Barotrauma var intervalRange = tunnel.Type != TunnelType.Cave ? GenerationParams.ResourceIntervalRange : GenerationParams.CaveResourceIntervalRange; do { - var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.Server); + var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.ServerAndClient); reachedLastNode = !CalculatePositionOnPath(); var id = Tunnels.IndexOf(tunnel) + ":" + nextPathPointId++; var spawnChance = tunnel.Type == TunnelType.Cave || tunnel.ParentTunnel?.Type == TunnelType.Cave ? @@ -2517,7 +2517,7 @@ namespace Barotrauma var containsResources = true; if (spawnChance < 1.0f) { - var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.Server); + var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.ServerAndClient); containsResources = spawnPointRoll <= spawnChance; } var tunnelType = tunnel.Type; @@ -2544,7 +2544,7 @@ namespace Barotrauma } int itemCount = 0; - string[] exclusiveResourceTags = new string[2] { "ore", "plant" }; + Identifier[] exclusiveResourceTags = new Identifier[2] { "ore".ToIdentifier(), "plant".ToIdentifier() }; // Create first cluster for each spawn point foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources)) @@ -2563,14 +2563,14 @@ namespace Barotrauma { var availablePathPoints = PathPoints.Where(p => p.ShouldContainResources && p.NextClusterProbability > 0 && - !excludedPathPointIds.Contains(p.Id)); + !excludedPathPointIds.Contains(p.Id)).ToList(); if (availablePathPoints.None()) { break; } var pathPoint = ToolBox.SelectWeightedRandom( - availablePathPoints.ToList(), + availablePathPoints, availablePathPoints.Select(p => p.NextClusterProbability).ToList(), - Rand.RandSync.Server); + Rand.RandSync.ServerAndClient); GenerateAdditionalCluster(pathPoint); } @@ -2580,9 +2580,9 @@ namespace Barotrauma while (itemCount < GenerationParams.ItemCount) { // We need to start filling some of the path points previously set to not contain resources - var availablePathPoints = PathPoints.Where(p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None()); - if (availablePathPoints.None()) { break; } - var pathPoint = availablePathPoints.GetRandom(randSync: Rand.RandSync.Server); + Func availablePathPoints = p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None(); + if (PathPoints.None(availablePathPoints)) { break; } + var pathPoint = PathPoints.GetRandom(availablePathPoints, randSync: Rand.RandSync.ServerAndClient); if (!GenerateFirstCluster(pathPoint)) { excludedPathPointIds.Add(pathPoint.Id); @@ -2716,7 +2716,7 @@ namespace Barotrauma if (validLocations.Any()) { - var location = validLocations.GetRandom(randSync: Rand.RandSync.Server); + var location = validLocations.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (CreateResourceCluster(pathPoint, location)) { var i = allValidLocations.FindIndex(l => l.Equals(location)); @@ -2764,7 +2764,7 @@ namespace Barotrauma selectedPrefab = ToolBox.SelectWeightedRandom( levelResources.Select(it => it.itemPrefab).ToList(), levelResources.Select(it => it.commonness).ToList(), - Rand.RandSync.Server); + Rand.RandSync.ServerAndClient); selectedPrefab.Tags.ForEach(t => { if (exclusiveResourceTags.Contains(t)) @@ -2781,7 +2781,7 @@ namespace Barotrauma selectedPrefab = ToolBox.SelectWeightedRandom( filteredResources.Select(it => it.itemPrefab).ToList(), filteredResources.Select(it => it.commonness).ToList(), - Rand.RandSync.Server); + Rand.RandSync.ServerAndClient); } if (selectedPrefab == null) { return false; } @@ -2800,7 +2800,7 @@ namespace Barotrauma if (maxClusterSize < 1) { return false; } var minClusterSize = Math.Min(GenerationParams.ResourceClusterSizeRange.X, maxClusterSize); - var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.Server); + var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.ServerAndClient); if (resourcesInCluster < 1) { return false; } @@ -2863,7 +2863,7 @@ namespace Barotrauma } } - var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.Server); + var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.ServerAndClient); var poiPos = poi.Position.ToVector2(); allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter) .CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter))); @@ -2969,11 +2969,11 @@ namespace Barotrauma var lerpAmount = 0.0f; for (int i = 1; i < resourceCount; i++) { - var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.Server); + var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient); lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLength.Value; lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f); } - var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.Server); + var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient); placedResources = new List(); for (int i = 0; i < resourceCount; i++) { @@ -2981,7 +2981,7 @@ namespace Barotrauma var item = new Item(resourcePrefab, selectedPos, submarine: null); Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)); - moveAmount += (item.GetComponent()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server); + moveAmount += (item.GetComponent()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient); item.Move(edgeNormal * moveAmount, ignoreContacts: true); if (item.GetComponent() is Holdable h) { @@ -3012,7 +3012,7 @@ namespace Barotrauma { TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter); - Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); + Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient); if (!cells.Any(c => c.IsPointInside(startPos + offset))) { startPos += offset; @@ -3081,7 +3081,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError(errorMsg); #endif - position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position; + position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; return false; } @@ -3122,7 +3122,7 @@ namespace Barotrauma return false; } - position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position; + position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; return true; } @@ -3366,9 +3366,9 @@ namespace Barotrauma private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type) { var tempSW = new Stopwatch(); - + // Min distance between a sub and the start/end/other sub. - float minDistance = Sonar.DefaultSonarRange; + const float minDistance = Sonar.DefaultSonarRange; var waypoints = WayPoint.WayPointList.Where(wp => wp.Submarine == null && wp.SpawnType == SpawnType.Path && @@ -3376,7 +3376,7 @@ namespace Barotrauma !IsCloseToStart(wp.WorldPosition, minDistance) && !IsCloseToEnd(wp.WorldPosition, minDistance)).ToList(); - var subDoc = SubmarineInfo.OpenFile(contentFile.Path); + var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. @@ -3419,7 +3419,7 @@ namespace Barotrauma { Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); tempSW.Restart(); - SubmarineInfo info = new SubmarineInfo(contentFile.Path) + SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) { Type = type }; @@ -3431,13 +3431,13 @@ namespace Barotrauma PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub)); foreach (Hull hull in sub.GetHulls(false)) { - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.WreckHullFloodingChance) + if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance) { - hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); + hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient); } } // Only spawn thalamus when the wreck has some thalamus items defined. - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) + if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) { if (!sub.CreateWreckAI()) { @@ -3550,7 +3550,7 @@ namespace Barotrauma spawnPoint = Vector2.Zero; while (waypoints.Any()) { - var wp = waypoints.GetRandom(Rand.RandSync.Server); + var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient); waypoints.Remove(wp); if (!IsBlocked(wp.WorldPosition, paddedDimensions)) { @@ -3665,17 +3665,19 @@ namespace Barotrauma { var totalSW = new Stopwatch(); totalSW.Start(); - var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList(); + var wreckFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToList(); if (wreckFiles.None()) { DebugConsole.ThrowError("No wreck files found in the selected content packages!"); return; } - wreckFiles.Shuffle(Rand.RandSync.Server); + wreckFiles.Shuffle(Rand.RandSync.ServerAndClient); int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, wreckFiles.Count); int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, wreckFiles.Count); - int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.Server); + int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.ServerAndClient); if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false) { @@ -3693,7 +3695,7 @@ namespace Barotrauma ContentFile contentFile = wreckFiles.First(); wreckFiles.RemoveAt(0); if (contentFile == null) { continue; } - string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); if (SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck) != null) { //placed successfully @@ -3701,7 +3703,7 @@ namespace Barotrauma } attempts++; } - + } totalSW.Stop(); Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)"); @@ -3743,8 +3745,10 @@ namespace Barotrauma private void CreateOutposts() { - var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost).ToList(); - if (!outpostFiles.Any() && !OutpostGenerationParams.Params.Any() && LevelData.ForceOutpostGenerationParams == null) + var outpostFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToList(); + if (!outpostFiles.Any() && !OutpostGenerationParams.OutpostParams.Any() && LevelData.ForceOutpostGenerationParams == null) { DebugConsole.ThrowError("No outpost files found in the selected content packages"); return; @@ -3768,7 +3772,7 @@ namespace Barotrauma Submarine outpost; if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) { - if (OutpostGenerationParams.Params.Any() || LevelData.ForceOutpostGenerationParams != null) + if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null) { Location location = i == 0 ? StartLocation : EndLocation; @@ -3779,31 +3783,29 @@ namespace Barotrauma } else { - var suitableParams = OutpostGenerationParams.Params - .Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); + var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); if (!suitableParams.Any()) { - suitableParams = OutpostGenerationParams.Params - .Where(p => location == null || !p.AllowedLocationTypes.Any()); + suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any()); + if (!suitableParams.Any()) + { + DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters."); + suitableParams = OutpostGenerationParams.OutpostParams; + } } - if (!suitableParams.Any()) - { - DebugConsole.ThrowError("No suitable outpost generation parameters found for the location type \"" + location.Type.Identifier + "\". Selecting random parameters."); - suitableParams = OutpostGenerationParams.Params; - } - outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.Server); + outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.ServerAndClient); } LocationType locationType = location?.Type; if (locationType == null) { - locationType = LocationType.List.GetRandom(Rand.RandSync.Server); + locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); if (outpostGenerationParams.AllowedLocationTypes.Any()) { - locationType = LocationType.List.Where(lt => - outpostGenerationParams.AllowedLocationTypes.Any(allowedType => - allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server); + locationType = LocationType.Prefabs.GetRandom(lt => + outpostGenerationParams.AllowedLocationTypes.Any(allowedType => + allowedType == "any" || lt.Identifier == allowedType), Rand.RandSync.ServerAndClient); } } @@ -3820,7 +3822,7 @@ namespace Barotrauma foreach (string categoryToHide in locationType.HideEntitySubcategories) { - foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.prefab?.HasSubCategory(categoryToHide) ?? false))) + foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false))) { entityToHide.HiddenInGame = true; } @@ -3830,8 +3832,8 @@ namespace Barotrauma { DebugConsole.NewMessage($"Loading a pre-built outpost for the {(isStart ? "start" : "end")} of the level..."); //backwards compatibility: if there are no generation params available, try to load an outpost file saved as a sub - ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server); - outpostInfo = new SubmarineInfo(outpostFile.Path) + ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.ServerAndClient); + outpostInfo = new SubmarineInfo(outpostFile.Path.Value) { Type = SubmarineType.Outpost }; @@ -3937,14 +3939,16 @@ namespace Barotrauma private void CreateBeaconStation() { if (!LevelData.HasBeaconStation) { return; } - var beaconStationFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.BeaconStation).ToList(); + var beaconStationFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToList(); if (beaconStationFiles.None()) { DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!"); return; } - var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.Server); - string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); + string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); if (BeaconStation == null) { return; } @@ -3976,10 +3980,7 @@ namespace Barotrauma Repairable repairable = reactorItem.GetComponent(); if (repairable != null) { - if (repairable != null) - { - repairable.DeteriorationSpeed = 0.0f; - } + repairable.DeteriorationSpeed = 0.0f; } } if (LevelData.IsBeaconActive) @@ -3988,7 +3989,7 @@ namespace Barotrauma reactorContainer.ContainableItemIdentifiers.Any() && ItemPrefab.Prefabs.ContainsKey(reactorContainer.ContainableItemIdentifiers.FirstOrDefault())) { ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItemIdentifiers.FirstOrDefault()]; - Spawner.AddToSpawnQueue( + Spawner.AddItemToSpawnQueue( fuelPrefab, reactorContainer.Inventory, onSpawned: (it) => reactorComponent.PowerUpImmediately()); } @@ -4007,7 +4008,7 @@ namespace Barotrauma foreach (Item item in reactorContainer.Inventory.AllItems) { if (item.NonInteractable) { continue; } - Spawner.AddToRemoveQueue(item); + Spawner.AddItemToRemoveQueue(item); } } @@ -4141,8 +4142,8 @@ namespace Barotrauma job ??= selectedPrefab.GetJobPrefab(); if (job == null) { continue; } - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server); - var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient); + var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; @@ -4163,7 +4164,7 @@ namespace Barotrauma bool TryGetExtraSpawnPoint(out Vector2 point) { point = Vector2.Zero; - var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(Rand.RandSync.Unsynced); + var hull = Hull.HullList.FindAll(h => h.Submarine == wreck).GetRandomUnsynced(); if (hull != null) { point = hull.WorldPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index eb59a650b..4fd6d7166 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -92,7 +92,7 @@ namespace Barotrauma OriginallyHadHuntingGrounds = element.GetAttributeBool("originallyhadhuntinggrounds", HasHuntingGrounds); string generationParamsId = element.GetAttributeString("generationparams", ""); - GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId); + GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || (!l.OldIdentifier.IsEmpty && l.OldIdentifier == generationParamsId)); if (GenerationParams == null) { DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\"."); @@ -106,18 +106,18 @@ namespace Barotrauma InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin); string biomeIdentifier = element.GetAttributeString("biome", ""); - Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier || b.OldIdentifier == biomeIdentifier); + Biome = Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeIdentifier || (!b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeIdentifier)); if (Biome == null) { DebugConsole.ThrowError($"Error in level data: could not find the biome \"{biomeIdentifier}\"."); - Biome = LevelGenerationParams.GetBiomes().First(); + Biome = Biome.Prefabs.First(); } string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { }); - EventHistory.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); + EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n))); string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); - NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); + NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); } @@ -129,7 +129,7 @@ namespace Barotrauma Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome); + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome.Identifier); Difficulty = locationConnection.Difficulty; float sizeFactor = MathUtils.InverseLerp( @@ -168,7 +168,7 @@ namespace Barotrauma Seed = location.BaseName; Biome = location.Biome; Type = LevelType.Outpost; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome); + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome.Identifier); Difficulty = 0.0f; var rand = new MTRandom(ToolBox.StringToInt(Seed)); @@ -183,7 +183,7 @@ namespace Barotrauma { if (string.IsNullOrEmpty(seed)) { - seed = Rand.Range(0, int.MaxValue, Rand.RandSync.Server).ToString(); + seed = Rand.Range(0, int.MaxValue, Rand.RandSync.ServerAndClient).ToString(); } Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -194,18 +194,18 @@ namespace Barotrauma if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); } var biome = - LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? - LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server); + Biome.Prefabs.FirstOrDefault(b => generationParams?.AllowedBiomeIdentifiers.Contains(b.Identifier) ?? false) ?? + Biome.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); var levelData = new LevelData( seed, - difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server), - Rand.Range(0.0f, 1.0f, Rand.RandSync.Server), + difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient), + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient), generationParams, biome); if (type == LevelType.LocationConnection) { - float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); + float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient); levelData.HasBeaconStation = beaconRng < 0.5f; levelData.IsBeaconActive = beaconRng > 0.25f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 4aec34f52..3ee01232f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -1,68 +1,20 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - class Biome + class LevelGenerationParams : PrefabWithUintIdentifier, ISerializableEntity { - public readonly string Identifier; - public readonly string OldIdentifier; - public readonly string DisplayName; - public readonly string Description; + public readonly static PrefabCollection LevelParams = new PrefabCollection(); - public readonly bool IsEndBiome; + public string Name => Identifier.Value; - public readonly List AllowedZones = new List(); - - public Biome(string name, string description) - { - Identifier = name; - Description = description; - } - - public Biome(XElement element) - { - Identifier = element.GetAttributeString("identifier", ""); - OldIdentifier = element.GetAttributeString("oldidentifier", null); - if (string.IsNullOrEmpty(Identifier)) - { - Identifier = element.GetAttributeString("name", ""); - DebugConsole.ThrowError("Error in biome \"" + Identifier + "\": identifier missing, using name as the identifier."); - } - - DisplayName = - TextManager.Get("biomename." + Identifier, returnNull: true) ?? - element.GetAttributeString("name", "Biome") ?? - TextManager.Get("biomename." + Identifier); - - Description = - TextManager.Get("biomedescription." + Identifier, returnNull: true) ?? - element.GetAttributeString("description", "") ?? - TextManager.Get("biomedescription." + Identifier); - - IsEndBiome = element.GetAttributeBool("endbiome", false); - - AllowedZones.AddRange(element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })); - } - } - - class LevelGenerationParams : ISerializableEntity - { - public static List LevelParams { get; private set; } - - private static List biomes; - - public string Name - { - get { return Identifier; } - } - - public readonly string Identifier; - - public readonly string OldIdentifier; + public Identifier OldIdentifier { get; } private int minWidth, maxWidth, height; @@ -100,48 +52,44 @@ namespace Barotrauma private int initialDepthMin, initialDepthMax; //which biomes can this type of level appear in - private readonly List allowedBiomes = new List(); + public readonly ImmutableHashSet AllowedBiomeIdentifiers; + public readonly bool AnyBiomeAllowed; - public IEnumerable AllowedBiomes - { - get { return allowedBiomes; } - } - - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; } - [Serialize(LevelData.LevelType.LocationConnection, true), Editable] + [Serialize(LevelData.LevelType.LocationConnection, IsPropertySaveable.Yes), Editable] public LevelData.LevelType Type { get; set; } - [Serialize("27,30,36", true), Editable] + [Serialize("27,30,36", IsPropertySaveable.Yes), Editable] public Color AmbientLightColor { get; set; } - [Serialize("20,40,50", true), Editable()] + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable()] public Color BackgroundTextureColor { get; set; } - [Serialize("20,40,50", true), Editable] + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] public Color BackgroundColor { get; set; } - [Serialize("255,255,255", true), Editable] + [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] public Color WallColor { get; @@ -149,7 +97,7 @@ namespace Barotrauma } private Vector2 startPosition; - [Serialize("0,0", true, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] + [Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 StartPosition { get { return startPosition; } @@ -162,7 +110,7 @@ namespace Barotrauma } private Vector2 endPosition; - [Serialize("1,0", true, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] + [Serialize("1,0", IsPropertySaveable.Yes, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 EndPosition { get { return endPosition; } @@ -174,70 +122,70 @@ namespace Barotrauma } } - [Serialize(true, true, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable] + [Serialize(true, IsPropertySaveable.Yes, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable] public bool CreateHoleNextToEnd { get; set; } - [Serialize(true, true, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable] + [Serialize(true, IsPropertySaveable.Yes, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable] public bool CreateHoleToAbyss { get; set; } - [Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] + [Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] public int LevelObjectAmount { get; set; } - [Serialize(80, true, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)] + [Serialize(80, IsPropertySaveable.Yes, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)] public int BackgroundCreatureAmount { get; set; } - [Serialize(100000, true), Editable] + [Serialize(100000, IsPropertySaveable.Yes), Editable] public int MinWidth { get { return minWidth; } set { minWidth = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(100000, true), Editable] + [Serialize(100000, IsPropertySaveable.Yes), Editable] public int MaxWidth { get { return maxWidth; } set { maxWidth = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(50000, true), Editable] + [Serialize(50000, IsPropertySaveable.Yes), Editable] public int Height { get { return height; } set { height = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMin { get { return initialDepthMin; } set { initialDepthMin = Math.Max(value, 0); } } - [Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMax { get { return initialDepthMax; } set { initialDepthMax = Math.Max(value, initialDepthMin); } } - [Serialize(6500, true), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] + [Serialize(6500, IsPropertySaveable.Yes), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] public int MinTunnelRadius { get; @@ -245,7 +193,7 @@ namespace Barotrauma } - [Serialize("0,1", true), Editable] + [Serialize("0,1", IsPropertySaveable.Yes), Editable] public Point SideTunnelCount { get; @@ -253,21 +201,21 @@ namespace Barotrauma } - [Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float SideTunnelVariance { get; set; } - [Serialize("2000,6000", true), Editable] + [Serialize("2000,6000", IsPropertySaveable.Yes), Editable] public Point MinSideTunnelRadius { get; set; } - [Editable, Serialize("3000, 3000", true, description: "How far from each other voronoi sites are placed. " + + [Editable, Serialize("3000, 3000", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " + "Sites determine shape of the voronoi graph which the level walls are generated from. " + "(Decreasing this value causes the number of sites, and the complexity of the level, to increase exponentially - be careful when adjusting)")] public Point VoronoiSiteInterval @@ -280,7 +228,7 @@ namespace Barotrauma } } - [Editable, Serialize("700,700", true, description: "How much random variation to apply to the positions of the voronoi sites on each axis. " + + [Editable, Serialize("700,700", IsPropertySaveable.Yes, description: "How much random variation to apply to the positions of the voronoi sites on each axis. " + "Small values produce roughly rectangular level walls. The larger the values are, the less uniform the shapes get.")] public Point VoronoiSiteVariance { @@ -293,7 +241,7 @@ namespace Barotrauma } } - [Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, true, description: "The edges of the individual wall cells are subdivided into edges of this size. " + [Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, IsPropertySaveable.Yes, description: "The edges of the individual wall cells are subdivided into edges of this size. " + "Can be used in conjunction with the rounding values to make the cells rounder. Smaller values will make the cells look smoother, " + "but make the level more performance-intensive as the number of polygons used in rendering and physics calculations increases.")] public int CellSubdivisionLength @@ -306,7 +254,7 @@ namespace Barotrauma } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, true, description: "How much the individual wall cells are rounded. " + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. " + "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")] public float CellRoundingAmount { @@ -317,7 +265,7 @@ namespace Barotrauma } } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, true, description: "How much random variance is applied to the edges of the cells. " + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. " + "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")] public float CellIrregularity { @@ -330,7 +278,7 @@ namespace Barotrauma [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), - Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] + Serialize("5000, 10000", IsPropertySaveable.Yes, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] public Point MainPathNodeIntervalRange { get { return mainPathNodeIntervalRange; } @@ -341,42 +289,42 @@ namespace Barotrauma } } - [Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float MainPathVariance { get; set; } - [Editable, Serialize(5, true, description: "The number of caves placed along the main path.")] + [Editable, Serialize(5, IsPropertySaveable.Yes, description: "The number of caves placed along the main path.")] public int CaveCount { get { return caveCount; } set { caveCount = MathHelper.Clamp(value, 0, 100); } } - [Serialize(100, true), Editable(MinValueInt = 0, MaxValueInt = 10000)] + [Serialize(100, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)] public int ItemCount { get; set; } - [Serialize("19200,38400", true, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)] + [Serialize("19200,38400", IsPropertySaveable.Yes, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)] public Point ResourceIntervalRange { get; set; } - [Serialize("9600,19200", true, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)] + [Serialize("9600,19200", IsPropertySaveable.Yes, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)] public Point CaveResourceIntervalRange { get; set; } - [Serialize("2,8", true, description: "The minimum and maximum amount of resources in a single cluster. " + + [Serialize("2,8", IsPropertySaveable.Yes, description: "The minimum and maximum amount of resources in a single cluster. " + "In addition to this, resource commonness affects the cluster size. Less common resources spawn in smaller clusters."), Editable(1, 20)] public Point ResourceClusterSizeRange { @@ -384,76 +332,76 @@ namespace Barotrauma set; } - [Serialize(0.3f, true, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.3f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float ResourceSpawnChance { get; set; } - [Serialize(1.0f, true, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float CaveResourceSpawnChance { get; set; } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] public int FloatingIceChunkCount { get; set; } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 100)] + [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 100)] public int IslandCount { get; set; } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] public int IceSpireCount { get; set; } - [Serialize(5, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(5, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] public int AbyssIslandCount { get; set; } - [Serialize("4000,7000", true), Editable] + [Serialize("4000,7000", IsPropertySaveable.Yes), Editable] public Point AbyssIslandSizeMin { get; set; } - [Serialize("8000,10000", true), Editable] + [Serialize("8000,10000", IsPropertySaveable.Yes), Editable] public Point AbyssIslandSizeMax { get; set; } - [Serialize(0.5f, true), Editable()] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable()] public float AbyssIslandCaveProbability { get; set; } - [Serialize(-300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] + [Serialize(-300000, IsPropertySaveable.Yes, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] public int SeaFloorDepth { get { return seaFloorBaseDepth; } set { seaFloorBaseDepth = MathHelper.Clamp(value, Level.MaxEntityDepth, 0); } } - [Serialize(1000, true, description: "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f)] + [Serialize(1000, IsPropertySaveable.Yes, description: "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f)] public int SeaFloorVariance { get { return seaFloorVariance; } set { seaFloorVariance = value; } } - [Serialize(0, true, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MountainCountMin { get { return mountainCountMin; } @@ -463,7 +411,7 @@ namespace Barotrauma } } - [Serialize(0, true, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MountainCountMax { get { return mountainCountMax; } @@ -473,7 +421,7 @@ namespace Barotrauma } } - [Serialize(1000, true, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(1000, IsPropertySaveable.Yes, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int MountainHeightMin { get { return mountainHeightMin; } @@ -483,7 +431,7 @@ namespace Barotrauma } } - [Serialize(5000, true, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(5000, IsPropertySaveable.Yes, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int MountainHeightMax { get { return mountainHeightMax; } @@ -493,72 +441,72 @@ namespace Barotrauma } } - [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } - [Serialize(1, true, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinWreckCount { get; set; } - [Serialize(1, true, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(1, IsPropertySaveable.Yes, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxWreckCount { get; set; } // TODO: Move the wreck parameters under a separate class? #region Wreck parameters - [Serialize(1, true, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MinCorpseCount { get; set; } - [Serialize(5, true, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MaxCorpseCount { get; set; } - [Serialize(0.0f, true, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float ThalamusProbability { get; set; } - [Serialize(0.5f, true, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckHullFloodingChance { get; set; } - [Serialize(0.1f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckFloodingHullMinWaterPercentage { get; set; } - [Serialize(1.0f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckFloodingHullMaxWaterPercentage { get; set; } #endregion - [Serialize(0.4f, true, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] + [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] public float BottomHoleProbability { get { return bottomHoleProbability; } set { bottomHoleProbability = MathHelper.Clamp(value, 0.0f, 1.0f); } } - [Serialize(1.0f, true, description: "Scale of the water particle texture."), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Scale of the water particle texture."), Editable] public float WaterParticleScale { get { return waterParticleScale; } private set { waterParticleScale = Math.Max(value, 0.01f); } } - [Serialize(2048.0f, true, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] + [Serialize(2048.0f, IsPropertySaveable.Yes, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] public float WallTextureSize { get; private set; } - [Serialize(2048.0f, true), Editable(minValue: 10.0f, maxValue: 10000.0f)] + [Serialize(2048.0f, IsPropertySaveable.Yes), Editable(minValue: 10.0f, maxValue: 10000.0f)] public float WallEdgeTextureWidth { get; private set; } - [Serialize(120.0f, true, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] public float WallEdgeExpandOutwardsAmount { get; private set; } - [Serialize(1000.0f, true, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)] + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)] public float WallEdgeExpandInwardsAmount { get; @@ -574,38 +522,31 @@ namespace Barotrauma public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } - public static IEnumerable GetBiomes() - { - return biomes; - } - - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Biome biome = null) + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Identifier biome = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); - if (LevelParams == null || !LevelParams.Any()) + if (!LevelParams.Any()) { - DebugConsole.ThrowError("Level generation presets not found - using default presets"); - return new LevelGenerationParams(null); + throw new InvalidOperationException("Level generation presets not found - using default presets"); } - var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any()); - if (biome == null) + var matchingLevelParams = LevelParams.Where(lp => + lp.Type == type && + (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && + !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); + matchingLevelParams = biome.IsEmpty + ? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)) + : matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome)); + + if (!matchingLevelParams.Any()) { - matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.All(b => b.IsEndBiome)); - } - else - { - matchingLevelParams = matchingLevelParams.FindAll(lp => lp.allowedBiomes.Contains(biome)); - } - if (matchingLevelParams.Count == 0) - { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{(biome?.Identifier ?? "null")}\", type: \"{type}\"!"); - if (biome != null) + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); + if (!biome.IsEmpty) { //try to find params that at least have a suitable type - matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type); - if (matchingLevelParams.Count == 0) + matchingLevelParams = LevelParams.Where(lp => lp.Type == type); + if (!matchingLevelParams.Any()) { //still not found, give up and choose some params randomly matchingLevelParams = LevelParams; @@ -613,53 +554,22 @@ namespace Barotrauma } } - return matchingLevelParams[Rand.Range(0, matchingLevelParams.Count, Rand.RandSync.Server)]; + return matchingLevelParams.GetRandom(Rand.RandSync.ServerAndClient); } - private LevelGenerationParams(XElement element) + public LevelGenerationParams(ContentXElement element, LevelGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) { - Identifier = element == null ? "default" : - element.GetAttributeString("identifier", null) ?? element.Name.ToString(); - OldIdentifier = element?.GetAttributeString("oldidentifier", null)?.ToLowerInvariant(); - Identifier = Identifier.ToLowerInvariant(); + OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - if (element == null) { return; } + if (element is null) { throw new ArgumentNullException($"{nameof(element)} is null"); } - string biomeStr = element.GetAttributeString("biomes", ""); - if (string.IsNullOrWhiteSpace(biomeStr) || biomeStr.Equals("any", StringComparison.OrdinalIgnoreCase)) - { - allowedBiomes = new List(biomes); - } - else - { - string[] biomeNames = biomeStr.Split(','); - for (int i = 0; i < biomeNames.Length; i++) - { - string biomeName = biomeNames[i].Trim().ToLowerInvariant(); - if (biomeName == "none") { continue; } + var allowedBiomeIdentifiers = element.GetAttributeIdentifierArray("biomes", Array.Empty()).ToHashSet(); + AnyBiomeAllowed = allowedBiomeIdentifiers.Contains("any".ToIdentifier()); + allowedBiomeIdentifiers.Remove("any".ToIdentifier()); + AllowedBiomeIdentifiers = allowedBiomeIdentifiers.ToImmutableHashSet(); - Biome matchingBiome = biomes.Find(b => - b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase) || (b.OldIdentifier?.Equals(biomeName, StringComparison.OrdinalIgnoreCase) ?? false)); - if (matchingBiome == null) - { - matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); - if (matchingBiome == null) - { - DebugConsole.ThrowError("Error in level generation parameters: biome \"" + biomeName + "\" not found."); - continue; - } - else - { - DebugConsole.NewMessage("Please use biome identifiers instead of names in level generation parameter \"" + Identifier + "\".", Color.Orange); - } - } - - allowedBiomes.Add(matchingBiome); - } - } - - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -691,87 +601,6 @@ namespace Barotrauma } } - public static void LoadPresets() - { - LevelParams = new List(); - biomes = new List(); - - var files = GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters); - if (!files.Any()) - { - files = new List() { new ContentFile("Content/Map/LevelGenerationParameters.xml", ContentType.LevelGenerationParameters) }; - } - - List biomeElements = new List(); - Dictionary levelParamElements = new Dictionary(); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - biomeElements.Clear(); - DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow); - } - else if (biomeElements.Any() && mainElement.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) - { - DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use tags to override the biomes."); - break; - } - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - if (isOverride) - { - if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) - { - biomeElements.Clear(); - biomeElements.AddRange(element.FirstElement().Elements()); - DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow); - } - else - { - string identifier = element.FirstElement().GetAttributeString("identifier", null) ?? element.GetAttributeString("name", ""); - if (levelParamElements.ContainsKey(identifier)) - { - DebugConsole.NewMessage($"Overriding the level generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow); - levelParamElements.Remove(identifier); - } - levelParamElements.Add(identifier, element.FirstElement()); - } - } - else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) - { - biomeElements.AddRange(element.Elements()); - } - else - { - string identifier = element.GetAttributeString("identifier", null) ?? element.GetAttributeString("name", ""); - if (levelParamElements.ContainsKey(identifier)) - { - DebugConsole.ThrowError($"Duplicate level generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use tags to override the generation parameters."); - continue; - } - else - { - levelParamElements.Add(identifier, element); - } - } - } - } - - foreach (XElement biomeElement in biomeElements) - { - biomes.Add(new Biome(biomeElement)); - } - - foreach (XElement levelParamElement in levelParamElements.Values) - { - LevelParams.Add(new LevelGenerationParams(levelParamElement)); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 673ce3a7b..1975dc33d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -81,7 +81,7 @@ namespace Barotrauma public string Name => Prefab?.Name ?? "LevelObject (null)"; - public Dictionary SerializableProperties { get; } = new Dictionary(); + public Dictionary SerializableProperties { get; } = new Dictionary(); public Level.Cave ParentCave; @@ -93,7 +93,7 @@ namespace Barotrauma Rotation = rotation; Health = prefab.Health; - spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1; + spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.ServerAndClient) : -1; if (Sprite != null && prefab.SpriteSpecificPhysicsBodyElements.ContainsKey(Sprite)) { @@ -115,7 +115,7 @@ namespace Barotrauma Physics.CollisionWall | Physics.CollisionCharacter; } - foreach (XElement triggerElement in prefab.LevelTriggerElements) + foreach (var triggerElement in prefab.LevelTriggerElements) { Triggers ??= new List(); Vector2 triggerPosition = triggerElement.GetAttributeVector2("position", Vector2.Zero) * scale; @@ -147,7 +147,7 @@ namespace Barotrauma if (overrideProperties == null) { continue; } if (overrideProperties.Sprites.Count > 0) { - spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.Server); + spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.ServerAndClient); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index dfd89203d..9bb8b1561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -7,7 +7,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Voronoi2; using Barotrauma.Extensions; @@ -140,7 +139,7 @@ namespace Barotrauma new GraphEdge(level.EndPosition - Vector2.UnitX, level.EndPosition + Vector2.UnitX), -Vector2.UnitY, LevelObjectPrefab.SpawnPosType.LevelEnd, Alignment.Top)); - var availablePrefabs = new List(LevelObjectPrefab.List); + var availablePrefabs =LevelObjectPrefab.Prefabs.OrderBy(p => p.UintIdentifier).ToList(); objects = new List(); updateableObjects = new List(); @@ -167,7 +166,7 @@ namespace Barotrauma suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); } - SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); + SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level); if (prefab.MaxCount < amount) @@ -181,7 +180,8 @@ namespace Barotrauma foreach (Level.Cave cave in level.Caves) { - availablePrefabs = new List(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall))); + availablePrefabs = LevelObjectPrefab.Prefabs.Where(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall)) + .OrderBy(p => p.UintIdentifier).ToList(); availableSpawnPositions.Clear(); suitableSpawnPositions.Clear(); spawnPositionWeights.Clear(); @@ -210,7 +210,7 @@ namespace Barotrauma spawnPositionWeights.Add(prefab, suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); } - SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); + SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level, cave); if (prefab.MaxCount < amount) @@ -228,7 +228,8 @@ namespace Barotrauma { Rand.SetSyncedSeed(ToolBox.StringToInt(level.Seed)); - var availablePrefabs = new List(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall))); + var availablePrefabs = LevelObjectPrefab.Prefabs.Where(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall)) + .OrderBy(p => p.UintIdentifier).ToList(); Dictionary> suitableSpawnPositions = new Dictionary>(); Dictionary> spawnPositionWeights = new Dictionary>(); @@ -258,7 +259,7 @@ namespace Barotrauma spawnPositionWeights.Add(prefab, suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); } - SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); + SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level); if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount) @@ -275,49 +276,49 @@ namespace Barotrauma { rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X)); } - rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server); + rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.ServerAndClient); Vector2 position = Vector2.Zero; Vector2 edgeDir = Vector2.UnitX; if (spawnPosition == null) { position = new Vector2( - Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server), - Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server)); + Rand.Range(0.0f, level.Size.X, Rand.RandSync.ServerAndClient), + Rand.Range(0.0f, level.Size.Y, Rand.RandSync.ServerAndClient)); } else { edgeDir = (spawnPosition.GraphEdge.Point1 - spawnPosition.GraphEdge.Point2) / spawnPosition.Length; - position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.Server); + position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.ServerAndClient); } if (!MathUtils.NearlyEqual(prefab.RandomOffset.X, 0.0f) || !MathUtils.NearlyEqual(prefab.RandomOffset.Y, 0.0f)) { - Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.Server); - position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.Server); + Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.ServerAndClient); + position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.ServerAndClient); } var newObject = new LevelObject(prefab, - new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation); + new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.ServerAndClient)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.ServerAndClient), rotation); AddObject(newObject, level); newObject.ParentCave = parentCave; foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects) { - int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.Server); + int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.ServerAndClient); for (int j = 0; j < childCount; j++) { - var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name)); + var matchingPrefabs = LevelObjectPrefab.Prefabs.Where(p => child.AllowedNames.Contains(p.Name)); int prefabCount = matchingPrefabs.Count(); - var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server)); + var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.ServerAndClient)); if (childPrefab == null) { continue; } - Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth; + Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient) * prefab.MinSurfaceWidth; var childObject = new LevelObject(childPrefab, - new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.Server)), - Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.Server), - rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server)); + new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.ServerAndClient)), + Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.ServerAndClient), + rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.ServerAndClient)); AddObject(childObject, level); childObject.ParentCave = parentCave; @@ -577,7 +578,7 @@ namespace Barotrauma if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server); + availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.ServerAndClient); } private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) @@ -585,7 +586,7 @@ namespace Barotrauma if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.Server); + availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.ServerAndClient); } public override void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index b9b83f5d7..9917943c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -1,14 +1,15 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - partial class LevelObjectPrefab : ISerializableEntity + partial class LevelObjectPrefab : PrefabWithUintIdentifier, ISerializableEntity { - public static List List { get; } = new List(); + public readonly static PrefabCollection Prefabs = new PrefabCollection(); public class ChildObject { @@ -24,7 +25,7 @@ namespace Barotrauma public ChildObject(XElement element) { - AllowedNames = element.GetAttributeStringArray("names", new string[0]).ToList(); + AllowedNames = element.GetAttributeStringArray("names", Array.Empty()).ToList(); MinCount = element.GetAttributeInt("mincount", 1); MaxCount = Math.Max(element.GetAttributeInt("maxcount", 1), MinCount); } @@ -58,13 +59,13 @@ namespace Barotrauma private set; } - [Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public float MinSize { get; private set; } - [Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public float MaxSize { get; @@ -74,14 +75,14 @@ namespace Barotrauma /// /// Which sides of a wall the object can appear on. /// - [Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), true, description: "Which sides of a wall the object can spawn on."), Editable] + [Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), IsPropertySaveable.Yes, description: "Which sides of a wall the object can spawn on."), Editable] public Alignment Alignment { get; private set; } - [Serialize(SpawnPosType.Wall, false), Editable()] + [Serialize(SpawnPosType.Wall, IsPropertySaveable.No), Editable()] public SpawnPosType SpawnPos { get; @@ -94,13 +95,13 @@ namespace Barotrauma private set; } - public readonly List LevelTriggerElements; + public readonly List LevelTriggerElements; /// /// Overrides the commonness of the object in a specific level type. /// Key = name of the level type, value = commonness in that level type. /// - public Dictionary OverrideCommonness; + public readonly Dictionary OverrideCommonness; public XElement PhysicsBodyElement { @@ -120,14 +121,14 @@ namespace Barotrauma } = new Dictionary(); - [Serialize(10000, false, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] + [Serialize(10000, IsPropertySaveable.No, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public int MaxCount { get; private set; } - [Serialize("0.0,1.0", true), Editable] + [Serialize("0.0,1.0", IsPropertySaveable.Yes), Editable] public Vector2 DepthRange { get; @@ -135,7 +136,7 @@ namespace Barotrauma } [Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f), - Serialize(0.0f, true, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")] + Serialize(0.0f, IsPropertySaveable.Yes, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")] /// /// The tendency for the prefab to form clusters. Used as an exponent for perlin noise values /// that are used to determine the probability for an object to spawn at a specific position. @@ -147,7 +148,7 @@ namespace Barotrauma } [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), - Serialize(0.0f, true, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " + + Serialize(0.0f, IsPropertySaveable.Yes, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " + " for an object to spawn at a specific position. Using the same (or close) value for different objects means the objects tend " + "to form clusters in the same areas.")] /// @@ -162,35 +163,35 @@ namespace Barotrauma private set; } - [Editable, Serialize("0,0", true, description: "Random offset from the surface the object spawns on.")] + [Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "Random offset from the surface the object spawns on.")] public Vector2 RandomOffset { get; private set; } - [Editable, Serialize(false, true, description: "Should the object be rotated to align it with the wall surface it spawns on.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the object be rotated to align it with the wall surface it spawns on.")] public bool AlignWithSurface { get; private set; } - [Editable, Serialize(true, true, description: "Can the object be placed near the start of the level.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the start of the level.")] public bool AllowAtStart { get; private set; } - [Editable, Serialize(true, true, description: "Can the object be placed near the end of the level.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the end of the level.")] public bool AllowAtEnd { get; private set; } - [Serialize(0.0f, true, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] /// /// Minimum length of a graph edge the object can spawn on. /// @@ -201,7 +202,7 @@ namespace Barotrauma } private Vector2 randomRotation; - [Editable, Serialize("0.0,0.0", true, description: "How much the rotation of the object can vary (min and max values in degrees).")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the rotation of the object can vary (min and max values in degrees).")] public Vector2 RandomRotation { get { return new Vector2(MathHelper.ToDegrees(randomRotation.X), MathHelper.ToDegrees(randomRotation.Y)); } @@ -214,7 +215,7 @@ namespace Barotrauma public Vector2 RandomRotationRad => randomRotation; private float swingAmount; - [Serialize(0.0f, true, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)] public float SwingAmount { get { return MathHelper.ToDegrees(swingAmount); } @@ -226,28 +227,28 @@ namespace Barotrauma public float SwingAmountRad => swingAmount; - [Serialize(0.0f, true, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SwingFrequency { get; private set; } - [Editable, Serialize("0.0,0.0", true, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")] public Vector2 ScaleOscillation { get; private set; } - [Serialize(0.0f, true, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float ScaleOscillationFrequency { get; private set; } - [Editable, Serialize(1.0f, true, description: "How likely it is for the object to spawn in a level. " + + [Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely it is for the object to spawn in a level. " + "This is relative to the commonness of the other objects - for example, having an object with " + "a commonness of 1 and another with a commonness of 10 would mean the latter appears in levels 10 times as frequently as the former. " + "The commonness value can be overridden on specific level types.")] @@ -257,45 +258,35 @@ namespace Barotrauma private set; } - [Serialize(0.0f, true, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SonarDisruption { get; private set; } - [Serialize(false, true, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable] public bool TakeLevelWallDamage { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HideWhenBroken { get; private set; } - [Serialize(100.0f, true), Editable] + [Serialize(100.0f, IsPropertySaveable.Yes), Editable] public float Health { get; private set; } - public string Identifier - { - get; - set; - } - - - public string Name - { - get { return Identifier; } - } + public string Name => Identifier.Value; public List ChildObjects { @@ -303,7 +294,7 @@ namespace Barotrauma private set; } - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; } @@ -323,91 +314,20 @@ namespace Barotrauma return "LevelObjectPrefab (" + Identifier + ")"; } - public static void LoadAll() - { - List.Clear(); - var files = GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs); - if (files.Count() > 0) - { - foreach (var file in files) - { - LoadConfig(file.Path); - } - } - else - { - LoadConfig("Content/LevelObjects/LevelObject/Prefabs.xml"); - } - } - - private static void LoadConfig(string configPath) - { - try - { - XDocument doc = XMLExtensions.TryLoadXml(configPath); - if (doc == null) { return; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - DebugConsole.NewMessage($"Overriding all level object prefabs with '{configPath}'", Color.Yellow); - List.Clear(); - } - else if (List.Any()) - { - DebugConsole.Log($"Loading additional level object prefabs from file '{configPath}'"); - } - foreach (XElement subElement in mainElement.Elements()) - { - var element = subElement.IsOverride() ? subElement.FirstElement() : subElement; - string identifier = element.GetAttributeString("identifier", ""); - var existingPrefab = List.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (existingPrefab != null) - { - if (subElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing level object prefab '{identifier}' using the file '{configPath}'", Color.Yellow); - List.Remove(existingPrefab); - } - else - { - DebugConsole.ThrowError($"Error in '{configPath}': Duplicate level object prefab '{identifier}' found in '{configPath}'! Each level object prefab must have a unique identifier. " + - "Use tags to override prefabs."); - continue; - } - } - List.Add(new LevelObjectPrefab(element)); - } - } - catch (Exception e) - { - DebugConsole.ThrowError(string.Format("Failed to load LevelObject prefabs from {0}", configPath), e); - } - } - public LevelObjectPrefab(XElement element, string identifier = null) + public LevelObjectPrefab(ContentXElement element, LevelObjectPrefabsFile file, Identifier identifierOverride = default) : base(file, ParseIdentifier(identifierOverride, element)) { ChildObjects = new List(); - LevelTriggerElements = new List(); + LevelTriggerElements = new List(); OverrideProperties = new List(); - OverrideCommonness = new Dictionary(); + OverrideCommonness = new Dictionary(); - Identifier = null; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (element != null) { Config = element; - Identifier = element.GetAttributeString("identifier", null) ?? identifier; - if (string.IsNullOrEmpty(Identifier)) - { -#if DEBUG - DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); -#else - DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); -#endif - Identifier = element.Name.ToString(); - } - LoadElements(element, -1); + + LoadElements(file, element, -1); InitProjSpecific(element); } @@ -419,10 +339,26 @@ namespace Barotrauma } } - private void LoadElements(XElement element, int parentTriggerIndex) + public static Identifier ParseIdentifier(Identifier identifierOverride, XElement element) + { + if (!identifierOverride.IsEmpty) { return identifierOverride; } + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + if (identifier.IsEmpty) + { +#if DEBUG + DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); +#else + DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); +#endif + identifier = element.NameAsIdentifier(); + } + return identifier; + } + + private void LoadElements(LevelObjectPrefabsFile file, ContentXElement element, int parentTriggerIndex) { int propertyOverrideCount = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -430,8 +366,8 @@ namespace Barotrauma var newSprite = new Sprite(subElement, lazyLoad: true); Sprites.Add(newSprite); var spriteSpecificPhysicsBodyElement = - subElement.Element("PhysicsBody") ?? subElement.Element("Body") ?? - subElement.Element("physicsbody") ?? subElement.Element("body"); + subElement.GetChildElement("PhysicsBody") ?? subElement.GetChildElement("Body") ?? + subElement.GetChildElement("physicsbody") ?? subElement.GetChildElement("body"); if (spriteSpecificPhysicsBodyElement != null) { SpriteSpecificPhysicsBodyElements.Add(newSprite, spriteSpecificPhysicsBodyElement); @@ -441,7 +377,7 @@ namespace Barotrauma DeformableSprite = new DeformableSprite(subElement, lazyLoad: true); break; case "overridecommonness": - string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); @@ -451,13 +387,13 @@ namespace Barotrauma case "trigger": OverrideProperties.Add(null); LevelTriggerElements.Add(subElement); - LoadElements(subElement, LevelTriggerElements.Count - 1); + LoadElements(file, subElement, LevelTriggerElements.Count - 1); break; case "childobject": ChildObjects.Add(new ChildObject(subElement)); break; case "overrideproperties": - var propertyOverride = new LevelObjectPrefab(subElement, identifier: Identifier + "-" + propertyOverrideCount); + var propertyOverride = new LevelObjectPrefab(subElement, file, identifierOverride: $"{Identifier}-{propertyOverrideCount}".ToIdentifier()); OverrideProperties[OverrideProperties.Count - 1] = propertyOverride; if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null) { @@ -475,12 +411,13 @@ namespace Barotrauma } } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public float GetCommonness(CaveGenerationParams generationParams, bool requireCaveSpecificOverride = true) { - if (generationParams?.Identifier != null && + if (generationParams != null && + generationParams.Identifier != Identifier.Empty && OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) { return commonness; @@ -490,13 +427,16 @@ namespace Barotrauma public float GetCommonness(LevelGenerationParams generationParams) { - if (generationParams?.Identifier != null && + if (generationParams != null && + generationParams.Identifier != Identifier.Empty && (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || - (generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + (!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) { return commonness; } return Commonness; } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 3ba23fb92..ec1d9fbe4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -184,7 +184,7 @@ namespace Barotrauma set; } - public string InfectIdentifier + public Identifier InfectIdentifier { get; set; @@ -199,7 +199,7 @@ namespace Barotrauma private bool triggeredOnce; private readonly bool triggerOnce; - public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") + public LevelTrigger(ContentXElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") { TriggererPosition = new Dictionary(); @@ -223,7 +223,7 @@ namespace Barotrauma cameraShake = element.GetAttributeFloat("camerashake", 0.0f); - InfectIdentifier = element.GetAttributeString("infectidentifier", null); + InfectIdentifier = element.GetAttributeIdentifier("infectidentifier", Identifier.Empty); InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f); triggerOnce = element.GetAttributeBool("triggeronce", false); @@ -264,7 +264,7 @@ namespace Barotrauma TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); - var tagsArray = element.GetAttributeStringArray("tags", new string[0]); + var tagsArray = element.GetAttributeStringArray("tags", Array.Empty()); foreach (string tag in tagsArray) { tags.Add(tag.ToLowerInvariant()); @@ -272,7 +272,7 @@ namespace Barotrauma if (triggeredBy.HasFlag(TriggererType.OtherTrigger)) { - var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]); + var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", Array.Empty()); foreach (string tag in otherTagsArray) { allowedOtherTriggerTags.Add(tag.ToLowerInvariant()); @@ -280,7 +280,7 @@ namespace Barotrauma } string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}"; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -317,12 +317,12 @@ namespace Barotrauma -sa * unrotatedForce.X + ca * unrotatedForce.Y); } - public static void LoadStatusEffect(List statusEffects, XElement element, string parentDebugName) + public static void LoadStatusEffect(List statusEffects, ContentXElement element, string parentDebugName) { statusEffects.Add(StatusEffect.Load(element, parentDebugName)); } - public static void LoadAttack(XElement element, string parentDebugName, bool triggerOnce, List attacks) + public static void LoadAttack(ContentXElement element, string parentDebugName, bool triggerOnce, List attacks) { var attack = new Attack(element, parentDebugName); if (!triggerOnce) @@ -574,7 +574,7 @@ namespace Barotrauma else if (triggerer is Submarine submarine) { ApplyAttacks(attacks, worldPosition, deltaTime); - if (!string.IsNullOrWhiteSpace(InfectIdentifier)) + if (!InfectIdentifier.IsEmpty) { submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 64dc5acb9..89c509288 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; +using Barotrauma.Extensions; #if DEBUG using System.Xml; #else @@ -20,93 +22,34 @@ namespace Barotrauma.RuinGeneration class RuinGenerationParams : OutpostGenerationParams { - public static List RuinParams - { - get - { - if (paramsList == null) - { - LoadAll(); - } - return paramsList; - } - } + public readonly static PrefabCollection RuinParams = + new PrefabCollection(); - private static List paramsList; + public override string Name => "RuinGenerationParams"; - private readonly string filePath; - - private RuinGenerationParams(XElement element, string filePath) : base(element, filePath) - { - this.filePath = filePath; - } - - public static RuinGenerationParams GetRandom(Rand.RandSync randSync = Rand.RandSync.Server) - { - if (paramsList == null) { LoadAll(); } - - if (paramsList.Count == 0) - { - DebugConsole.ThrowError("No ruin configuration files found in any content package."); - return new RuinGenerationParams(null, null); - } - - return paramsList[Rand.Int(paramsList.Count, randSync)]; - } - - private static void LoadAll() - { - paramsList = new List(); - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc?.Root == null) { continue; } - - foreach (XElement subElement in doc.Root.Elements()) - { - var mainElement = subElement; - if (subElement.IsOverride()) - { - mainElement = subElement.FirstElement(); - paramsList.Clear(); - DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow); - } - else if (paramsList.Any()) - { - DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'"); - } - var newParams = new RuinGenerationParams(mainElement, configFile.Path); - paramsList.Add(newParams); - } - } - } - - public static void ClearAll() - { - paramsList?.Clear(); - paramsList = null; - } + public RuinGenerationParams(ContentXElement element, RuinConfigFile file) : base(element, file) { } public static void SaveAll() { + #warning TODO: revise System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true }; - + foreach (RuinGenerationParams generationParams in RuinParams) { - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) + foreach (RuinConfigFile configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { - if (configFile.Path != generationParams.filePath) { continue; } + if (configFile.Path != generationParams.ContentFile.Path) { continue; } XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } SerializableProperty.SerializeProperties(generationParams, doc.Root); - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -114,5 +57,7 @@ namespace Barotrauma.RuinGeneration } } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index ef4e0c1cb..72504a631 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -67,7 +67,7 @@ namespace Barotrauma.RuinGeneration if (interestingPosCount == 0) { //make sure there's at least one PositionsOfInterest in the ruins - level.PositionsOfInterest.Add(new Level.InterestingPosition(waypoints.GetRandom(Rand.RandSync.Server).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); + level.PositionsOfInterest.Add(new Level.InterestingPosition(waypoints.GetRandom(Rand.RandSync.ServerAndClient).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 7c26aea92..0ace84cde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.IO; +using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -21,10 +23,26 @@ namespace Barotrauma } public readonly SubmarineInfo subInfo; - - public LinkedSubmarinePrefab(SubmarineInfo subInfo) + + public override Sprite Sprite => null; + + public override string OriginalName => Name.Value; + + public override LocalizedString Name => subInfo.Name; + + public override ImmutableHashSet Tags => null; + + public override ImmutableHashSet AllowedLinks => null; + + public override MapEntityCategory Category => MapEntityCategory.Misc; + + public override ImmutableHashSet Aliases { get; } + + public LinkedSubmarinePrefab(SubmarineInfo subInfo) : base(subInfo.Name.ToIdentifier()) { this.subInfo = subInfo; + + Aliases = Name.Value.ToEnumerable().ToImmutableHashSet(); } protected override void CreateInstance(Rectangle rect) @@ -71,6 +89,8 @@ namespace Barotrauma return true; } } + + public int CargoCapacity { get; private set; } public LinkedSubmarine(Submarine submarine, ushort id = Entity.NullEntityID) : base(null, submarine, id) @@ -110,6 +130,7 @@ namespace Barotrauma { LinkedSubmarine sl = new LinkedSubmarine(mainSub, id); sl.GenerateWallVertices(element); + sl.CargoCapacity = element.GetAttributeInt("cargocapacity", 0); if (sl.wallVertices.Any()) { sl.Rect = new Rectangle( @@ -152,7 +173,7 @@ namespace Barotrauma if (element.Name != "Structure") { continue; } string name = element.GetAttributeString("name", ""); - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = Structure.FindPrefab(name, identifier); if (prefab == null) { continue; } @@ -173,7 +194,7 @@ namespace Barotrauma } // LinkedSubmarine.Load() is called from MapEntity.LoadAll() - public static LinkedSubmarine Load(XElement element, Submarine submarine, IdRemap idRemap) + public static LinkedSubmarine Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); LinkedSubmarine linkedSub; @@ -206,14 +227,16 @@ namespace Barotrauma } } - linkedSub.filePath = element.GetAttributeString("filepath", ""); - int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]); + #warning TODO: revise + linkedSub.filePath = element.GetAttributeContentPath("filepath")?.Value ?? string.Empty; + int[] linkedToIds = element.GetAttributeIntArray("linkedto", Array.Empty()); for (int i = 0; i < linkedToIds.Length; i++) { linkedSub.linkedToID.Add(idRemap.GetOffsetId(linkedToIds[i])); } linkedSub.originalLinkedToID = idRemap.GetOffsetId(element.GetAttributeInt("originallinkedto", 0)); linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0); + linkedSub.CargoCapacity = element.GetAttributeInt("cargocapacity", 0); return linkedSub.loadSub ? linkedSub : null; } @@ -269,7 +292,7 @@ namespace Barotrauma DockingPort linkedPort = null; DockingPort myPort = null; - MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent() != null); + MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt as Item)?.GetComponent() != null); if (linkedItem == null) { linkedPort = DockingPort.List.FirstOrDefault(dp => dp.DockingTarget != null && dp.DockingTarget.Item.Submarine == sub); @@ -349,7 +372,7 @@ namespace Barotrauma wall.SetDamage(i, 0, createNetworkEvent: false); } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != sub) { continue; } hull.WaterVolume = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index d3275a394..a6884d570 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -16,10 +16,10 @@ namespace Barotrauma { public readonly ushort OriginalID; public readonly ushort ModuleIndex; - public readonly string Identifier; + public readonly Identifier Identifier; public readonly int OriginalContainerIndex; - public TakenItem(string identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex) + public TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex) { OriginalID = originalID; OriginalContainerIndex = originalContainerIndex; @@ -34,7 +34,7 @@ namespace Barotrauma OriginalContainerIndex = item.OriginalContainerIndex; OriginalID = item.ID; ModuleIndex = (ushort) item.OriginalModuleIndex; - Identifier = item.prefab.Identifier; + Identifier = ((MapEntity)item).Prefab.Identifier; } public bool IsEqual(TakenItem obj) @@ -46,11 +46,11 @@ namespace Barotrauma { if (item.OriginalContainerIndex != Entity.NullEntityID) { - return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; + return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier; } else { - return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; + return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier; } } } @@ -252,7 +252,7 @@ namespace Barotrauma return $"Location ({Name ?? "null"})"; } - public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType? forceLocationType = null, IEnumerable existingLocations = null) + public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable existingLocations = null) { Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost); Name = RandomName(Type, rand, existingLocations); @@ -263,22 +263,21 @@ namespace Barotrauma public Location(XElement element) { - string locationType = element.GetAttributeString("type", ""); - Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)); + Identifier locationType = element.GetAttributeIdentifier("type", ""); + Type = LocationType.Prefabs[locationType]; bool typeNotFound = false; if (Type == null) { //turn lairs into abandoned outposts - if (locationType.Equals("lair", StringComparison.OrdinalIgnoreCase)) + if (locationType == "lair") { - Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("Abandoned", StringComparison.OrdinalIgnoreCase)); + Type ??= LocationType.Prefabs["Abandoned"]; addInitialMissionsForType = Type; } if (Type == null) { DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead."); - Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase)); - Type ??= LocationType.List.First(); + Type ??= LocationType.Prefabs["None"] ?? LocationType.Prefabs.First(); } if (Type != null) { @@ -287,8 +286,8 @@ namespace Barotrauma typeNotFound = true; } - string originalLocationType = element.GetAttributeString("originaltype", locationType); - OriginalType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)); + Identifier originalLocationType = element.GetAttributeIdentifier("originaltype", locationType); + OriginalType = LocationType.Prefabs[locationType]; baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); @@ -312,7 +311,7 @@ namespace Barotrauma LoadLocationTypeChange(element); } - string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]); + string[] takenItemStr = element.GetAttributeStringArray("takenitems", Array.Empty()); foreach (string takenItem in takenItemStr) { string[] takenItemSplit = takenItem.Split(';'); @@ -336,15 +335,15 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\""); continue; } - takenItems.Add(new TakenItem(takenItemSplit[0], id, containerIndex, moduleIndex)); + takenItems.Add(new TakenItem(takenItemSplit[0].ToIdentifier(), id, containerIndex, moduleIndex)); } - killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", new int[0]).ToHashSet(); + killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty()).ToHashSet(); System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationType}\"!"); if (Type == null) { - Type = LocationType.List.First(); + Type = LocationType.Prefabs.First(); } LevelData = new LevelData(element.Element("Level")); @@ -359,7 +358,7 @@ namespace Barotrauma { TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0); LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0); - foreach (XElement subElement in locationElement.Elements()) + foreach (var subElement in locationElement.Elements()) { switch (subElement.Name.ToString()) { @@ -377,8 +376,8 @@ namespace Barotrauma } else { - string missionIdentifier = subElement.GetAttributeString("missionidentifier", ""); - var mission = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase)); + Identifier missionIdentifier = subElement.GetAttributeIdentifier("missionidentifier", ""); + var mission = MissionPrefab.Prefabs[missionIdentifier]; if (mission == null) { DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found."); @@ -400,7 +399,7 @@ namespace Barotrauma { var id = childElement.GetAttributeString("prefabid", null); if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = MissionPrefab.List.Find(p => p.Identifier.Equals(id, StringComparison.OrdinalIgnoreCase)); + var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id); if (prefab == null) { continue; } var destination = childElement.GetAttributeInt("destinationindex", -1); var selected = childElement.GetAttributeBool("selected", false); @@ -410,7 +409,7 @@ namespace Barotrauma } - public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType? forceLocationType = null, IEnumerable existingLocations = null) + public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable existingLocations = null) { return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } @@ -432,11 +431,11 @@ namespace Barotrauma if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom()); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandomUnsynced()); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom()); + UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); } CreateStore(force: true); @@ -446,11 +445,11 @@ namespace Barotrauma { if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.Server)); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.ServerAndClient)); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.Server)); + UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.ServerAndClient)); } } @@ -474,11 +473,11 @@ namespace Barotrauma #endif } - public MissionPrefab UnlockMissionByIdentifier(string identifier) + public MissionPrefab UnlockMissionByIdentifier(Identifier identifier) { - if (AvailableMissions.Any(m => m.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))) { return null; } + if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } - var missionPrefab = MissionPrefab.List.Find(mp => mp.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier); if (missionPrefab == null) { DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found."); @@ -500,9 +499,9 @@ namespace Barotrauma return null; } - public MissionPrefab UnlockMissionByTag(string tag) + public MissionPrefab UnlockMissionByTag(Identifier tag) { - var matchingMissions = MissionPrefab.List.FindAll(mp => mp.Tags.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase))); + var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag)); if (!matchingMissions.Any()) { DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions not found."); @@ -623,11 +622,11 @@ namespace Barotrauma { if (addInitialMissionsForType.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandom()); + UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced()); } if (addInitialMissionsForType.MissionTags.Any()) { - UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandom()); + UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced()); } addInitialMissionsForType = null; } @@ -661,7 +660,7 @@ namespace Barotrauma public LocationType GetLocationType() { - if (IsCriticallyRadiated() && LocationType.List.FirstOrDefault(lt => lt.Identifier.Equals(Type.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newLocationType) + if (IsCriticallyRadiated() && LocationType.Prefabs[Type.ReplaceInRadiation] is { } newLocationType) { return newLocationType; } @@ -757,8 +756,8 @@ namespace Barotrauma List specials = new List(); foreach (var childElement in element.GetChildElements("item")) { - var id = childElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } + var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); + if (id.IsEmpty) { continue; } var prefab = ItemPrefab.Find(null, id); if (prefab == null) { continue; } specials.Add(prefab); @@ -1045,9 +1044,9 @@ namespace Barotrauma RequestedGoods.Clear(); for (int i = 0; i < RequestedGoodsCount; i++) { - var item = ItemPrefab.Prefabs.GetRandom(p => + var item = ItemPrefab.Prefabs.GetRandom(p => p.CanBeSold && !RequestedGoods.Contains(p) && - p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial); + p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced); if (item == null) { break; } RequestedGoods.Add(item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 792cce9bc..2bfa64c65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -7,23 +7,24 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { - class LocationType + class LocationType : PrefabWithUintIdentifier { - public static readonly List List = new List(); + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + private readonly List names; private readonly List portraits = new List(); // - private readonly List> hireableJobs; + private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; private readonly float totalHireableWeight; public Dictionary CommonnessPerZone = new Dictionary(); - public readonly string Identifier; - public readonly string Name; + public readonly LocalizedString Name; public readonly float BeaconStationChance; @@ -31,8 +32,8 @@ namespace Barotrauma public readonly List CanChangeTo = new List(); - public readonly List MissionIdentifiers = new List(); - public readonly List MissionTags = new List(); + public readonly ImmutableArray MissionIdentifiers; + public readonly ImmutableArray MissionTags; public readonly List HideEntitySubcategories = new List(); @@ -44,7 +45,15 @@ namespace Barotrauma private set; } - public List NameFormats { get; private set; } + private ImmutableArray? nameFormats = null; + public IReadOnlyList NameFormats + { + get + { + nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray(); + return nameFormats; + } + } public bool HasHireableCharacters { @@ -104,32 +113,30 @@ namespace Barotrauma return $"LocationType (" + Identifier + ")"; } - private LocationType(XElement element) + public LocationType(ContentXElement element, LocationTypesFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) { - Identifier = element.GetAttributeString("identifier", element.Name.ToString()); - Name = TextManager.Get("LocationName." + Identifier, fallBackTag: "unknown"); + Name = TextManager.Get("LocationName." + Identifier, "unknown"); BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - NameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); - MissionIdentifiers = element.GetAttributeStringArray("missionidentifiers", new string[0]).ToList(); - MissionTags = element.GetAttributeStringArray("missiontags", new string[0]).ToList(); + MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); + MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty()).ToImmutableArray(); - HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList(); + HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", Array.Empty()).ToList(); ReplaceInRadiation = element.GetAttributeString(nameof(ReplaceInRadiation).ToLower(), ""); string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); - string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt"); + ContentPath nameFile = element.GetAttributeContentPath("namefile") ?? ContentPath.FromRaw(null, "Content/Map/locationNames.txt"); try { - names = File.ReadAllLines(nameFile).ToList(); + names = File.ReadAllLines(nameFile.Value).ToList(); } catch (Exception e) { @@ -151,31 +158,16 @@ namespace Barotrauma CommonnessPerZone[zoneIndex] = zoneCommonness; } - hireableJobs = new List>(); - foreach (XElement subElement in element.Elements()) + var hireableJobs = new List<(Identifier, float)>(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "hireable": - string jobIdentifier = subElement.GetAttributeString("identifier", ""); - JobPrefab jobPrefab = null; - if (jobIdentifier == "") - { - DebugConsole.ThrowError("Error in location type \""+ Identifier + "\" - hireable jobs should be configured using identifiers instead of names."); - } - else - { - jobPrefab = JobPrefab.Get(jobIdentifier.ToLowerInvariant()); - } - if (jobPrefab == null) - { - DebugConsole.ThrowError("Error in in location type " + Identifier + " - could not find a job with the identifier \"" + jobIdentifier + "\"."); - continue; - } + Identifier jobIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f); totalHireableWeight += jobCommonness; - Tuple hireableJob = new Tuple(jobPrefab, jobCommonness); - hireableJobs.Add(hireableJob); + hireableJobs.Add((jobIdentifier, jobCommonness)); break; case "symbol": Sprite = new Sprite(subElement, lazyLoad: true); @@ -216,16 +208,17 @@ namespace Barotrauma break; } } + this.hireableJobs = hireableJobs.ToImmutableArray(); } public JobPrefab GetRandomHireable() { - float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.Server); + float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.ServerAndClient); - foreach (Tuple hireable in hireableJobs) + foreach ((Identifier jobIdentifier, float commonness) in hireableJobs) { - if (randFloat < hireable.Item2) return hireable.Item1; - randFloat -= hireable.Item2; + if (randFloat < commonness) { return JobPrefab.Prefabs[jobIdentifier]; } + randFloat -= commonness; } return null; @@ -252,12 +245,13 @@ namespace Barotrauma public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false) { - Debug.Assert(List.Count > 0, "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); + Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); - List allowedLocationTypes = - List.FindAll(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)); + LocationType[] allowedLocationTypes = + Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)) + .OrderBy(p => p.UintIdentifier).ToArray(); - if (allowedLocationTypes.Count == 0) + if (allowedLocationTypes.Length == 0) { DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!"); } @@ -266,82 +260,15 @@ namespace Barotrauma { return ToolBox.SelectWeightedRandom( allowedLocationTypes, - allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToList(), + allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToArray(), rand); } else { - return allowedLocationTypes[rand.Next() % allowedLocationTypes.Count]; + return allowedLocationTypes[rand.Next() % allowedLocationTypes.Length]; } } - public static void Init() - { - List.Clear(); - var locationTypeFiles = GameMain.Instance.GetFilesOfType(ContentType.LocationTypes); - if (!locationTypeFiles.Any()) - { - DebugConsole.ThrowError("No location types configured in any of the selected content packages. Attempting to load from the vanilla content package..."); - locationTypeFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), ContentType.LocationTypes); - if (!locationTypeFiles.Any()) - { - throw new Exception("No location types configured in any of the selected content packages. Please try uninstalling mods or reinstalling the game."); - } - } - - foreach (ContentFile file in locationTypeFiles) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - DebugConsole.NewMessage($"Overriding all location types with '{file.Path}'", Color.Yellow); - List.Clear(); - } - else if (List.Any()) - { - DebugConsole.NewMessage($"Loading additional location types from file '{file.Path}'"); - } - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement; - bool allowOverriding = false; - if (sourceElement.IsOverride()) - { - element = sourceElement.FirstElement(); - allowOverriding = true; - } - string identifier = element.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"Error in '{file.Path}': No identifier defined for {element.Name.ToString()}"); - continue; - } - var duplicate = List.FirstOrDefault(l => l.Identifier == identifier); - if (duplicate != null) - { - if (allowOverriding) - { - List.Remove(duplicate); - DebugConsole.NewMessage($"Overriding the location type with the identifier '{identifier}' with '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': Duplicate identifier defined with the identifier '{identifier}'"); - continue; - } - } - LocationType locationType = new LocationType(element); - List.Add(locationType); - } - } - - foreach (EventSet eventSet in EventSet.List) - { - eventSet.CheckLocationTypeErrors(); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 4f7c3424c..0639ceb6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -1,8 +1,10 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -21,7 +23,7 @@ namespace Barotrauma /// /// The change can only happen if there's at least one of the given types of locations near this one /// - public readonly List RequiredLocations; + public readonly ImmutableArray RequiredLocations; /// /// How close the location needs to be to one of the RequiredLocations for the change to occur @@ -55,7 +57,7 @@ namespace Barotrauma public Requirement(XElement element, LocationTypeChange change) { - RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList(); + RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); @@ -124,9 +126,9 @@ namespace Barotrauma } } - public readonly string CurrentType; + public readonly Identifier CurrentType; - public readonly string ChangeToType; + public readonly Identifier ChangeToType; /// /// Base probability per turn for the location to change if near one of the RequiredLocations @@ -137,12 +139,33 @@ namespace Barotrauma public List Requirements = new List(); - public List Messages = new List(); + private readonly bool requireChangeMessages; + private readonly string messageTag; + private ImmutableArray? messages = null; + public IReadOnlyList Messages + { + get + { + if (!messages.HasValue) + { + messages = TextManager.GetAll(messageTag).ToImmutableArray(); + if (messages.Value.None()) + { + if (requireChangeMessages) + { + DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); + } + } + } + + return messages.Value; + } + } /// /// The change can't happen if there's one or more of the given types of locations near this one /// - public readonly List DisallowedAdjacentLocations; + public readonly ImmutableArray DisallowedAdjacentLocations; /// /// How close the location needs to be to one of the DisallowedAdjacentLocations for the change to be disabled @@ -156,14 +179,14 @@ namespace Barotrauma public readonly Point RequiredDurationRange; - public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) + public LocationTypeChange(Identifier currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) { CurrentType = currentType; - ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", "")); + ChangeToType = element.GetAttributeIdentifier("type", element.GetAttributeIdentifier("to", "")); RequireDiscovered = element.GetAttributeBool("requirediscovered", false); - DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList(); + DisallowedAdjacentLocations = element.GetAttributeIdentifierArray("disallowedadjacentlocations", Array.Empty()).ToImmutableArray(); DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1); RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero); @@ -184,19 +207,10 @@ namespace Barotrauma RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0)); } - string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType); + this.requireChangeMessages = requireChangeMessages; + messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType); - Messages = TextManager.GetAll(messageTag); - if (Messages == null) - { - if (requireChangeMessages) - { - DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType); - } - Messages = new List(); - } - - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("requirement", StringComparison.OrdinalIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index a5929ded1..f7b648463 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -88,7 +88,7 @@ namespace Barotrauma bool lairsFound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -112,11 +112,11 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(!Locations.Contains(null)); for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); } List connectionElements = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -133,10 +133,10 @@ namespace Barotrauma Locations[locationIndices.Y].Connections.Add(connection); connection.LevelData = new LevelData(subElement.Element("Level")); string biomeId = subElement.GetAttributeString("biome", ""); - connection.Biome = - LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ?? - LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.OldIdentifier == biomeId) ?? - LevelGenerationParams.GetBiomes().First(); + connection.Biome = + Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ?? + Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ?? + Biome.Prefabs.First(); Connections.Add(connection); connectionElements.Add(subElement); break; @@ -214,13 +214,13 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); } foreach (Location location in Locations) { - if (!location.Type.Identifier.Equals("city", StringComparison.OrdinalIgnoreCase) && - !location.Type.Identifier.Equals("outpost", StringComparison.OrdinalIgnoreCase)) + if (location.Type.Identifier != "city" && + location.Type.Identifier != "outpost") { continue; } @@ -252,8 +252,8 @@ namespace Barotrauma for (float y = 10.0f; y < Height - 10.0f; y += generationParams.VoronoiSiteInterval.Y) { voronoiSites.Add(new Vector2( - x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server), - y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server))); + x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient), + y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient))); } } @@ -297,12 +297,12 @@ namespace Barotrauma Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 }; - int positionIndex = Rand.Int(1, Rand.RandSync.Server); + int positionIndex = Rand.Int(1, Rand.RandSync.ServerAndClient); Vector2 position = points[positionIndex]; if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; } int zone = GetZoneIndex(position.X); - newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, existingLocations: Locations); + newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient), requireOutpost: false, existingLocations: Locations); Locations.Add(newLocations[i]); } @@ -394,7 +394,7 @@ namespace Barotrauma connectionsBetweenZones[i] = new List(); } var shuffledConnections = Connections.ToList(); - shuffledConnections.Shuffle(Rand.RandSync.Server); + shuffledConnections.Shuffle(Rand.RandSync.ServerAndClient); foreach (var connection in shuffledConnections) { int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X); @@ -447,9 +447,10 @@ namespace Barotrauma Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ? Connections[i].Locations[0] : Connections[i].Locations[1]; - if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase)) + if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") { - leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost && !lt.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase))); + #warning TODO: determinism? + leftMostLocation.ChangeType(LocationType.Prefabs.First(lt => lt.HasOutpost && lt.Identifier != "abandoned")); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; @@ -473,10 +474,10 @@ namespace Barotrauma foreach (LocationConnection connection in Connections) { //float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width); - //connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f); + //connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.ServerAndClient), 1.2f, 100.0f); float difficulty = connection.CenterPos.X / Width * 100; float random = difficulty > 10 ? 5 : 0; - connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.Server), 1.0f, 100.0f); + connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.ServerAndClient), 1.0f, 100.0f); } AssignBiomes(); @@ -522,21 +523,13 @@ namespace Barotrauma { float zoneWidth = Width / generationParams.DifficultyZones; int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1; - if (zoneIndex < 1) - { - return LevelGenerationParams.GetBiomes().First(); - - } - else if (zoneIndex >= generationParams.DifficultyZones) - { - return LevelGenerationParams.GetBiomes().Last(); - } - return LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex)); + zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1); + return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex)); } private void AssignBiomes() { - var biomes = LevelGenerationParams.GetBiomes(); + var biomes = Biome.Prefabs; float zoneWidth = Width / generationParams.DifficultyZones; List allowedBiomes = new List(10); @@ -550,7 +543,7 @@ namespace Barotrauma { if (location.MapPosition.X < zoneX) { - location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.Server)]; + location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.ServerAndClient)]; } } } @@ -692,10 +685,10 @@ namespace Barotrauma if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } }) { - metadata.SetValue("campaign.location.id", CurrentLocationIndex); - metadata.SetValue("campaign.location.name", CurrentLocation.Name); - metadata.SetValue("campaign.location.biome", CurrentLocation.Biome?.Identifier ?? "null"); - metadata.SetValue("campaign.location.type", CurrentLocation.Type?.Identifier ?? "null"); + metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex); + metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name); + metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier()); + metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier()); } } @@ -999,7 +992,7 @@ namespace Barotrauma { string prevName = location.Name; - var newType = LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)); + var newType = LocationType.Prefabs[change.ChangeToType]; if (newType == null) { DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); @@ -1053,7 +1046,7 @@ namespace Barotrauma return; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1083,14 +1076,14 @@ namespace Barotrauma } } - string locationType = subElement.GetAttributeString("type", ""); + Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); string prevLocationName = location.Name; LocationType prevLocationType = location.Type; - LocationType newLocationType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)) ?? LocationType.List.First(); + LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); location.ChangeType(newLocationType); if (showNotifications && prevLocationType != location.Type) { - var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase)); + var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier); if (change != null) { ChangeLocationTypeProjSpecific(location, prevLocationName, change); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs index 3c47acbd5..38ee2f1dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs @@ -1,30 +1,31 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - class MapGenerationParams : ISerializableEntity + class MapGenerationParams : Prefab, ISerializableEntity { - private static MapGenerationParams instance; - private static string loadedFile; + public static readonly PrefabSelector Params = new PrefabSelector(); public static MapGenerationParams Instance { get { - return instance; + return Params.ActivePrefab; } } #if DEBUG - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool ShowLocations { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool ShowLevelTypeNames { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool ShowOverlay { get; set; } #else public readonly bool ShowLocations = true; @@ -32,70 +33,70 @@ namespace Barotrauma public readonly bool ShowOverlay = true; #endif - [Serialize(6, true)] + [Serialize(6, IsPropertySaveable.Yes)] public int DifficultyZones { get; set; } //Number of difficulty zones - [Serialize(8000, true), Editable] + [Serialize(8000, IsPropertySaveable.Yes), Editable] public int Width { get; set; } - [Serialize(500, true), Editable] + [Serialize(500, IsPropertySaveable.Yes), Editable] public int Height { get; set; } - [Serialize(20.0f, true, description: "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] + [Serialize(20.0f, IsPropertySaveable.Yes, description: "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] public float SmallLevelConnectionLength { get; set; } - [Serialize(200.0f, true, description: "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] + [Serialize(200.0f, IsPropertySaveable.Yes, description: "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] public float LargeLevelConnectionLength { get; set; } - [Serialize("20,20", true, description: "How far from each other voronoi sites are placed. " + + [Serialize("20,20", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " + "Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " + "(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable] public Point VoronoiSiteInterval { get; set; } - [Serialize("5,5", true), Editable] + [Serialize("5,5", IsPropertySaveable.Yes), Editable] public Point VoronoiSiteVariance { get; set; } - [Serialize(10.0f, true, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)] public float MinConnectionDistance { get; set; } - [Serialize(5.0f, true, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)] + [Serialize(5.0f, IsPropertySaveable.Yes, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)] public float MinLocationDistance { get; set; } - [Serialize(0.1f, true, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] public float ConnectionIndicatorIterationMultiplier { get; set; } - [Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] public float ConnectionIndicatorDisplacementMultiplier { get; set; } - public int[] GateCount { get; private set; } + public readonly ImmutableArray GateCount; #if CLIENT - [Serialize(0.75f, true), Editable(DecimalCount = 2)] + [Serialize(0.75f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float MinZoom { get; set; } - [Serialize(1.5f, true), Editable(DecimalCount = 2)] + [Serialize(1.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float MaxZoom { get; set; } - [Serialize(1.0f, true), Editable(DecimalCount = 2)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float MapTileScale { get; set; } - [Serialize(15.0f, true, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] + [Serialize(15.0f, IsPropertySaveable.Yes, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] public float LocationIconSize { get; set; } - [Serialize(5.0f, true, description: "Width of the connections between locations, in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] + [Serialize(5.0f, IsPropertySaveable.Yes, description: "Width of the connections between locations, in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] public float LocationConnectionWidth { get; set; } - [Serialize("220,220,100,255", true, description: "The color used to display the indicators (current location, selected location, etc)."), Editable()] + [Serialize("220,220,100,255", IsPropertySaveable.Yes, description: "The color used to display the indicators (current location, selected location, etc)."), Editable()] public Color IndicatorColor { get; set; } - [Serialize("150,150,150,255", true, description: "The color used to display the connections between locations."), Editable()] + [Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections between locations."), Editable()] public Color ConnectionColor { get; set; } - [Serialize("150,150,150,255", true, description: "The color used to display the connections between locations when they're highlighted."), Editable()] + [Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections between locations when they're highlighted."), Editable()] public Color HighlightedConnectionColor { get; set; } - [Serialize("150,150,150,255", true, description: "The color used to display the connections the player hasn't travelled through."), Editable()] + [Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections the player hasn't travelled through."), Editable()] public Color UnvisitedConnectionColor { get; set; } public Sprite ConnectionSprite { get; private set; } @@ -110,110 +111,36 @@ namespace Barotrauma public Sprite CurrentLocationIndicator { get; private set; } public Sprite SelectedLocationIndicator { get; private set; } - private readonly Dictionary> mapTiles = new Dictionary>(); - public Dictionary> MapTiles - { - get { return mapTiles; } - } + public readonly ImmutableDictionary> MapTiles; #endif - public string Name - { - get { return GetType().ToString(); } - } + public string Name => GetType().ToString(); - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; } public RadiationParams RadiationParams; - public static void Init() - { - - var files = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.MapGenerationParameters); - if (!files.Any()) - { - DebugConsole.ThrowError("No map generation parameters found in the selected content packages!"); - return; - } - // Let's not actually load the parameters until we have solved which file is the last, because loading the parameters takes some resources that would also need to be released. - XElement selectedElement = null; - string selectedFile = null; - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - if (selectedElement != null) - { - DebugConsole.NewMessage($"Overriding the map generation parameters with '{file.Path}'", Color.Yellow); - } - } - else if (selectedElement != null) - { - DebugConsole.ThrowError($"Error in {file.Path}: Another map generation parameter file already loaded! Use tags to override it."); - break; - } - selectedElement = mainElement; - selectedFile = file.Path; - } - - if (selectedFile == loadedFile) { return; } - -#if CLIENT - if (instance != null) - { - instance?.ConnectionSprite?.Remove(); - instance?.PassedConnectionSprite?.Remove(); - instance?.SelectedLocationIndicator?.Remove(); - instance?.CurrentLocationIndicator?.Remove(); - instance?.DecorativeGraphSprite?.Remove(); - instance?.MissionIcon?.Remove(); - instance?.TypeChangeIcon?.Remove(); - instance?.FogOfWarSprite?.Remove(); - foreach (List spriteList in instance.mapTiles.Values) - { - foreach (Sprite sprite in spriteList) - { - sprite.Remove(); - } - } - instance.mapTiles.Clear(); - } -#endif - instance = null; - - if (selectedElement == null) - { - DebugConsole.ThrowError("Could not find a valid element in the map generation parameter files!"); - } - else - { - instance = new MapGenerationParams(selectedElement); - loadedFile = selectedFile; - } - } - - private MapGenerationParams(XElement element) + public MapGenerationParams(ContentXElement element, MapGenerationParametersFile file) : base(file, file.Path.Value.ToIdentifier()) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - GateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null); - if (GateCount == null) + var gateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null); + if (gateCount == null) { - GateCount = new int[DifficultyZones]; + gateCount = new int[DifficultyZones]; for (int i = 0; i < DifficultyZones; i++) { - GateCount[i] = 1; + gateCount[i] = 1; } } + GateCount = gateCount.ToImmutableArray(); - foreach (XElement subElement in element.Elements()) + Dictionary> mapTiles = new Dictionary>(); + + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -225,7 +152,7 @@ namespace Barotrauma PassedConnectionSprite = new Sprite(subElement); break; case "maptile": - string biome = subElement.GetAttributeString("biome", ""); + Identifier biome = subElement.GetAttributeIdentifier("biome", ""); if (!mapTiles.ContainsKey(biome)) { mapTiles[biome] = new List(); @@ -257,6 +184,30 @@ namespace Barotrauma break; } } +#if CLIENT + MapTiles = mapTiles.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); +#endif + } + + public override void Dispose() + { +#if CLIENT + ConnectionSprite?.Remove(); + PassedConnectionSprite?.Remove(); + SelectedLocationIndicator?.Remove(); + CurrentLocationIndicator?.Remove(); + DecorativeGraphSprite?.Remove(); + MissionIcon?.Remove(); + TypeChangeIcon?.Remove(); + FogOfWarSprite?.Remove(); + foreach (ImmutableArray spriteList in MapTiles.Values) + { + foreach (Sprite sprite in spriteList) + { + sprite.Remove(); + } + } +#endif } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index 9fd7a488e..1e1e136c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -12,18 +12,18 @@ namespace Barotrauma { public string Name => nameof(Radiation); - [Serialize(defaultValue: 0f, isSaveable: true)] + [Serialize(defaultValue: 0f, isSaveable: IsPropertySaveable.Yes)] public float Amount { get; set; } - [Serialize(defaultValue: true, isSaveable: true)] + [Serialize(defaultValue: true, isSaveable: IsPropertySaveable.Yes)] public bool Enabled { get; set; } - public Dictionary SerializableProperties { get; } + public Dictionary SerializableProperties { get; } public readonly Map Map; public readonly RadiationParams Params; - private Affliction radiationAffliction; + private Affliction? radiationAffliction; private float radiationTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs index b61811521..20933077b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs @@ -7,39 +7,39 @@ namespace Barotrauma internal class RadiationParams: ISerializableEntity { public string Name => nameof(RadiationParams); - public Dictionary SerializableProperties { get; } + public Dictionary SerializableProperties { get; } - [Serialize(defaultValue: -100f, isSaveable: false, "How much radiation the world starts with.")] + [Serialize(defaultValue: -100f, isSaveable: IsPropertySaveable.No, "How much radiation the world starts with.")] public float StartingRadiation { get; set; } - [Serialize(defaultValue: 100f, isSaveable: false, "How much radiation is added on each step.")] + [Serialize(defaultValue: 100f, isSaveable: IsPropertySaveable.No, "How much radiation is added on each step.")] public float RadiationStep { get; set; } - [Serialize(defaultValue: 10, isSaveable: false, "How many turns in radiation does it take for an outpost to be removed from the map.")] + [Serialize(defaultValue: 10, isSaveable: IsPropertySaveable.No, "How many turns in radiation does it take for an outpost to be removed from the map.")] public int CriticalRadiationThreshold { get; set; } - [Serialize(defaultValue: 3, isSaveable: false, "Minimum amount of outposts in the level that cannot be removed due to radiation.")] + [Serialize(defaultValue: 3, isSaveable: IsPropertySaveable.No, "Minimum amount of outposts in the level that cannot be removed due to radiation.")] public int MinimumOutpostAmount { get; set; } - [Serialize(defaultValue: 3f, isSaveable: false, "How fast the radiation increase animation goes.")] + [Serialize(defaultValue: 3f, isSaveable: IsPropertySaveable.No, "How fast the radiation increase animation goes.")] public float AnimationSpeed { get; set; } - [Serialize(defaultValue: 10f, isSaveable: false, "How long it takes to apply more radiation damage while in a radiated zone.")] + [Serialize(defaultValue: 10f, isSaveable: IsPropertySaveable.No, "How long it takes to apply more radiation damage while in a radiated zone.")] public float RadiationDamageDelay { get; set; } - [Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")] + [Serialize(defaultValue: 1f, isSaveable: IsPropertySaveable.No, "How much is the radiation affliction increased by while in a radiated zone.")] public float RadiationDamageAmount { get; set; } - [Serialize(defaultValue: -1.0f, isSaveable: false, "Maximum amount of radiation.")] + [Serialize(defaultValue: -1.0f, isSaveable: IsPropertySaveable.No, "Maximum amount of radiation.")] public float MaxRadiation { get; set; } - [Serialize(defaultValue: "139,0,0,85", isSaveable: false, "The color of the radiated area.")] + [Serialize(defaultValue: "139,0,0,85", isSaveable: IsPropertySaveable.No, "The color of the radiated area.")] public Color RadiationAreaColor { get; set; } - [Serialize(defaultValue: "255,0,0,255", isSaveable: false, "The tint of the radiation border sprites.")] + [Serialize(defaultValue: "255,0,0,255", isSaveable: IsPropertySaveable.No, "The tint of the radiation border sprites.")] public Color RadiationBorderTint { get; set; } - [Serialize(defaultValue: 16.66f, isSaveable: false, "Speed of the border spritesheet animation.")] + [Serialize(defaultValue: 16.66f, isSaveable: IsPropertySaveable.No, "Speed of the border spritesheet animation.")] public float BorderAnimationSpeed { get; set; } public RadiationParams(XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 48da9e5a5..fb989ec73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public static List mapEntityList = new List(); - public readonly MapEntityPrefab prefab; + public readonly MapEntityPrefab Prefab; protected List linkedToID; public List unresolvedLinkedToID; @@ -27,23 +27,22 @@ namespace Barotrauma /// protected readonly List Upgrades = new List(); - public HashSet disallowedUpgrades = new HashSet(); - - [Editable, Serialize("", true)] + public readonly HashSet DisallowedUpgradeSet = new HashSet(); + + [Editable, Serialize("", IsPropertySaveable.Yes)] public string DisallowedUpgrades { - get { return string.Join(",", disallowedUpgrades); } + get { return string.Join(",", DisallowedUpgradeSet); } set { - disallowedUpgrades.Clear(); + DisallowedUpgradeSet.Clear(); if (!string.IsNullOrWhiteSpace(value)) { string[] splitTags = value.Split(','); foreach (string tag in splitTags) { string[] splitTag = tag.Trim().Split(':'); - splitTag[0] = splitTag[0].ToLowerInvariant(); - disallowedUpgrades.Add(string.Join(":", splitTag)); + DisallowedUpgradeSet.Add(string.Join(":", splitTag).ToIdentifier()); } } } @@ -108,19 +107,19 @@ namespace Barotrauma get { return false; } } - public List AllowedLinks => prefab == null ? new List() : prefab.AllowedLinks; + public IEnumerable AllowedLinks => Prefab == null ? Enumerable.Empty() : Prefab.AllowedLinks; public bool ResizeHorizontal { - get { return prefab != null && prefab.ResizeHorizontal; } + get { return Prefab != null && Prefab.ResizeHorizontal; } } public bool ResizeVertical { - get { return prefab != null && prefab.ResizeVertical; } + get { return Prefab != null && Prefab.ResizeVertical; } } //for upgrading the dimensions of the entity from xml - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int RectWidth { get { return rect.Width; } @@ -131,7 +130,7 @@ namespace Barotrauma } } //for upgrading the dimensions of the entity from xml - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int RectHeight { get { return rect.Height; } @@ -147,7 +146,7 @@ namespace Barotrauma public bool SpriteDepthOverrideIsSet { get; private set; } public float SpriteOverrideDepth => SpriteDepth; private float _spriteOverrideDepth = float.NaN; - [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)] + [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, IsPropertySaveable.Yes)] public float SpriteDepth { get @@ -166,10 +165,10 @@ namespace Barotrauma } } - [Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] public virtual float Scale { get; set; } = 1; - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool HiddenInGame { get; @@ -225,14 +224,14 @@ namespace Barotrauma } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool RemoveIfLinkedOutpostDoorInUse { get; protected set; } = true; - [Serialize("", true, "Submarine editor layer")] + [Serialize("", IsPropertySaveable.Yes, "Submarine editor layer")] public string Layer { get; set; } /// @@ -249,7 +248,7 @@ namespace Barotrauma public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id) { - this.prefab = prefab; + this.Prefab = prefab; Scale = prefab != null ? prefab.Scale : 1; } @@ -303,12 +302,12 @@ namespace Barotrauma return (Submarine.RectContains(WorldRect, position)); } - public bool HasUpgrade(string identifier) + public bool HasUpgrade(Identifier identifier) { return GetUpgrade(identifier) != null; } - public Upgrade GetUpgrade(string identifier) + public Upgrade GetUpgrade(Identifier identifier) { return Upgrades.Find(upgrade => upgrade.Identifier == identifier); } @@ -331,7 +330,7 @@ namespace Barotrauma { AddUpgrade(upgrade, createNetworkEvent); } - DebugConsole.Log($"Set (ID: {ID} {prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}"); + DebugConsole.Log($"Set (ID: {ID} {Prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}"); } /// @@ -344,7 +343,7 @@ namespace Barotrauma return false; } - if (disallowedUpgrades.Contains(upgrade.Identifier)) { return false; } + if (DisallowedUpgradeSet.Contains(upgrade.Identifier)) { return false; } Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier); @@ -560,7 +559,7 @@ namespace Barotrauma /// public static void UpdateAll(float deltaTime, Camera cam) { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.Update(deltaTime, cam); } @@ -635,7 +634,7 @@ namespace Barotrauma IdRemap idRemap = new IdRemap(parentElement, idOffset); List entities = new List(); - foreach (XElement element in parentElement.Elements()) + foreach (var element in parentElement.Elements()) { string typeName = element.Name.ToString(); @@ -658,7 +657,7 @@ namespace Barotrauma if (t == typeof(Structure)) { string name = element.Attribute("name").Value; - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier); if (structurePrefab == null) { @@ -672,7 +671,7 @@ namespace Barotrauma try { - MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) }); + MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(ContentXElement), typeof(Submarine), typeof(IdRemap) }); if (loadMethod == null) { DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + "."); @@ -683,7 +682,7 @@ namespace Barotrauma } else { - object newEntity = loadMethod.Invoke(t, new object[] { element, submarine, idRemap }); + object newEntity = loadMethod.Invoke(t, new object[] { element.FromPackage(null), submarine, idRemap }); if (newEntity != null) { entities.Add((MapEntity)newEntity); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 6596c4067..7a7fa2598 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -1,8 +1,10 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -23,7 +25,7 @@ namespace Barotrauma Legacy = 1024 } - abstract partial class MapEntityPrefab : IPrefab, IDisposable + abstract partial class MapEntityPrefab : PrefabWithUintIdentifier { public static IEnumerable List { @@ -51,207 +53,15 @@ namespace Barotrauma } } - protected string originalName; - protected string identifier; - - public Sprite sprite; + //which prefab has been selected for placing + public static MapEntityPrefab Selected { get; set; } //the position where the structure is being placed (needed when stretching the structure) protected static Vector2 placePosition; - protected ConstructorInfo constructor; - - //is it possible to stretch the entity horizontally/vertically - [Serialize(false, false)] - public bool ResizeHorizontal { get; protected set; } - - [Serialize(false, false)] - public bool ResizeVertical { get; protected set; } - - //which prefab has been selected for placing - protected static MapEntityPrefab selected; - - public string OriginalName - { - get { return originalName; } - } - - public virtual string Name - { - get { return originalName; } - } - - public string GetItemNameTextId() - { - var textId = $"entityname.{Identifier}"; - return TextManager.ContainsTag(textId) ? textId : null; - } - - public string GetHullNameTextId() - { - var textId = $"roomname.{Identifier}"; - return TextManager.ContainsTag(textId) ? textId : null; - } - - //Used to differentiate between items when saving/loading - //Allows changing the name of an item without breaking existing subs or having multiple items with the same name - public string Identifier - { - get { return identifier; } - } - - public string FilePath { get; protected set; } - - public ContentPackage ContentPackage { get; protected set; } - - public HashSet Tags - { - get; - protected set; - } = new HashSet(); - - public static MapEntityPrefab Selected - { - get { return selected; } - set { selected = value; } - } - - [Serialize("", false)] - public string Description - { - get; - protected set; - } - - [Serialize("", false)] - public string AllowedUpgrades { get; set; } - - [Serialize(false, false)] - public bool HideInMenus { get; set; } - - [Serialize("", false)] - public string Subcategory { get; set; } - - [Serialize(false, false)] - public bool Linkable - { - get; - protected set; - } - - /// - /// Links defined to identifiers. - /// - public List AllowedLinks { get; protected set; } = new List(); - - public MapEntityCategory Category - { - get; - protected set; - } - - [Serialize("1.0,1.0,1.0,1.0", false)] - public Color SpriteColor - { - get; - protected set; - } - - [Serialize(1f, true), Editable(0.1f, 10f, DecimalCount = 3)] - public float Scale { get; protected set; } - - //If a matching prefab is not found when loading a sub, the game will attempt to find a prefab with a matching alias. - //(allows changing names while keeping backwards compatibility with older sub files) - public HashSet Aliases - { - get; - protected set; - } - - public static void Init() - { - CoreEntityPrefab ep = new CoreEntityPrefab - { - identifier = "hull", - originalName = TextManager.Get("EntityName.hull"), - Description = TextManager.Get("EntityDescription.hull"), - constructor = typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), - ResizeHorizontal = true, - ResizeVertical = true, - Linkable = true - }; - ep.AllowedLinks.Add("hull"); - ep.Aliases = new HashSet { "hull" }; - CoreEntityPrefab.Prefabs.Add(ep, false); - - ep = new CoreEntityPrefab - { - identifier = "gap", - originalName = TextManager.Get("EntityName.gap"), - Description = TextManager.Get("EntityDescription.gap"), - constructor = typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), - ResizeHorizontal = true, - ResizeVertical = true - }; - CoreEntityPrefab.Prefabs.Add(ep, false); - ep.Aliases = new HashSet { "gap" }; - - ep = new CoreEntityPrefab - { - identifier = "waypoint", - originalName = TextManager.Get("EntityName.waypoint"), - Description = TextManager.Get("EntityDescription.waypoint"), - constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }) - }; - CoreEntityPrefab.Prefabs.Add(ep, false); - ep.Aliases = new HashSet { "waypoint" }; - - ep = new CoreEntityPrefab - { - identifier = "spawnpoint", - originalName = TextManager.Get("EntityName.spawnpoint"), - Description = TextManager.Get("EntityDescription.spawnpoint"), - constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }) - }; - CoreEntityPrefab.Prefabs.Add(ep, false); - ep.Aliases = new HashSet { "spawnpoint" }; - } - - public abstract void Dispose(); - - public MapEntityPrefab() - { - Category = MapEntityCategory.Structure; - } - - public string[] GetAllowedUpgrades() - { - return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(","); - } - - public bool HasSubCategory(string subcategory) - { - return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false; - } - - protected virtual void CreateInstance(Rectangle rect) - { - if (constructor == null) return; - object[] lobject = new object[] { this, rect }; - constructor.Invoke(lobject); - } - -#if DEBUG - public void DebugCreateInstance() - { - Rectangle rect = new Rectangle(new Point((int)Screen.Selected.Cam.WorldViewCenter.X, (int)Screen.Selected.Cam.WorldViewCenter.Y), new Point((int)Submarine.GridSize.X, (int)Submarine.GridSize.Y)); - CreateInstance(rect); - } -#endif - public static bool SelectPrefab(object selection) { - if ((selected = selection as MapEntityPrefab) != null) + if ((Selected = selection as MapEntityPrefab) != null) { placePosition = Vector2.Zero; return true; @@ -262,37 +72,45 @@ namespace Barotrauma } } + //a method that allows the GUIListBoxes to check through a delegate if the entityprefab is still selected + public static object GetSelected() + { + return (object)Selected; + } + /// /// Find a matching map entity prefab /// /// The name of the item (can be omitted when searching based on identifier) /// The identifier of the item (if null, the identifier is ignored and the search is done only based on the name) + [Obsolete("Prefer MapEntityPrefab.FindByIdentifier or MapEntityPrefab.FindByName")] public static MapEntityPrefab Find(string name, string identifier = null, bool showErrorMessages = true) { - if (name != null) - { - name = name.ToLowerInvariant(); - } + return Find(name, (identifier ?? "").ToIdentifier(), showErrorMessages); + } + [Obsolete("Prefer MapEntityPrefab.FindByIdentifier or MapEntityPrefab.FindByName")] + public static MapEntityPrefab Find(string name, Identifier identifier, bool showErrorMessages = true) + { //try to search based on identifier first - if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(identifier)) + if (string.IsNullOrEmpty(name) && !identifier.IsEmpty) { - foreach (MapEntityPrefab prefab in List) - { - if (prefab.identifier == identifier) { return prefab; } - } + if (CoreEntityPrefab.Prefabs.ContainsKey(identifier)) { return CoreEntityPrefab.Prefabs[identifier]; } + if (StructurePrefab.Prefabs.ContainsKey(identifier)) { return StructurePrefab.Prefabs[identifier]; } + if (ItemPrefab.Prefabs.ContainsKey(identifier)) { return ItemPrefab.Prefabs[identifier]; } + if (ItemAssemblyPrefab.Prefabs.ContainsKey(identifier)) { return ItemAssemblyPrefab.Prefabs[identifier]; } } foreach (MapEntityPrefab prefab in List) { - if (identifier != null) + if (!identifier.IsEmpty) { - if (prefab.identifier != identifier) + if (prefab.Identifier != identifier) { - if (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(identifier, StringComparison.OrdinalIgnoreCase))) + if (prefab.Aliases != null && prefab.Aliases.Any(a => a == identifier)) { return prefab; - } + } continue; } else @@ -302,8 +120,8 @@ namespace Barotrauma } if (!string.IsNullOrEmpty(name)) { - if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || - prefab.originalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))) { return prefab; @@ -332,28 +150,133 @@ namespace Barotrauma return List.FirstOrDefault(p => predicate(p)); } + + public static MapEntityPrefab FindByName(string name) + { + if (name.IsNullOrEmpty()) { throw new ArgumentException($"{nameof(name)} must not be null or empty"); } + + return Find(prefab => + prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + (prefab.Aliases != null && + prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))); + } + + public static MapEntityPrefab FindByIdentifier(Identifier identifier) + => CoreEntityPrefab.Prefabs.TryGet(identifier, out var corePrefab) ? corePrefab + : ItemPrefab.Prefabs.TryGet(identifier, out var itemPrefab) ? itemPrefab + : StructurePrefab.Prefabs.TryGet(identifier, out var structurePrefab) ? structurePrefab + : ItemAssemblyPrefab.Prefabs.TryGet(identifier, out var itemAssemblyPrefab) ? itemAssemblyPrefab + : (MapEntityPrefab)null; + + public abstract Sprite Sprite { get; } + + public abstract string OriginalName { get; } + + public abstract LocalizedString Name { get; } + + public abstract ImmutableHashSet Tags { get; } + + /// + /// Links defined to identifiers. + /// + public abstract ImmutableHashSet AllowedLinks { get; } + + public abstract MapEntityCategory Category { get; } + + //If a matching prefab is not found when loading a sub, the game will attempt to find a prefab with a matching alias. + //(allows changing names while keeping backwards compatibility with older sub files) + public abstract ImmutableHashSet Aliases { get; } + + //is it possible to stretch the entity horizontally/vertically + [Serialize(false, IsPropertySaveable.No)] + public bool ResizeHorizontal { get; protected set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool ResizeVertical { get; protected set; } + + [Serialize("", IsPropertySaveable.No)] + public LocalizedString Description { get; protected set; } + + [Serialize("", IsPropertySaveable.No)] + public string AllowedUpgrades { get; protected set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool HideInMenus { get; protected set; } + + [Serialize("", IsPropertySaveable.No)] + public string Subcategory { get; protected set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool Linkable { get; protected set; } + + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)] + public Color SpriteColor { get; protected set; } + + [Serialize(1f, IsPropertySaveable.Yes), Editable(0.1f, 10f, DecimalCount = 3)] + public float Scale { get; protected set; } + + protected MapEntityPrefab(Identifier identifier) : base(null, identifier) { } + + public MapEntityPrefab(ContentXElement element, ContentFile file) : base(file, element) { } + + public string GetItemNameTextId() + { + var textId = $"entityname.{Identifier}"; + return TextManager.ContainsTag(textId) ? textId : null; + } + + public string GetHullNameTextId() + { + var textId = $"roomname.{Identifier}"; + return TextManager.ContainsTag(textId) ? textId : null; + } + + private string cachedAllowedUpgrades = ""; + private ImmutableHashSet allowedUpgradeSet; + public IEnumerable GetAllowedUpgrades() + { + if (string.IsNullOrWhiteSpace(AllowedUpgrades)) { return Enumerable.Empty(); } + if (allowedUpgradeSet is null || cachedAllowedUpgrades != AllowedUpgrades) + { + allowedUpgradeSet = AllowedUpgrades.Split(",").ToIdentifiers().ToImmutableHashSet(); + cachedAllowedUpgrades = AllowedUpgrades; + } + + return allowedUpgradeSet; + } + + public bool HasSubCategory(string subcategory) + { + return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false; + } + + protected abstract void CreateInstance(Rectangle rect); + +#if DEBUG + public void DebugCreateInstance() + { + Rectangle rect = new Rectangle(new Point((int)Screen.Selected.Cam.WorldViewCenter.X, (int)Screen.Selected.Cam.WorldViewCenter.Y), new Point((int)Submarine.GridSize.X, (int)Submarine.GridSize.Y)); + CreateInstance(rect); + } +#endif + /// /// Check if the name or any of the aliases of this prefab match the given name. /// - public bool NameMatches(string name, StringComparison comparisonType) => originalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType))); + public bool NameMatches(string name, StringComparison comparisonType) => OriginalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType))); public bool NameMatches(IEnumerable allowedNames, StringComparison comparisonType) => allowedNames.Any(n => NameMatches(n, comparisonType)); public bool IsLinkAllowed(MapEntityPrefab target) { if (target == null) { return false; } - if (target is StructurePrefab && AllowedLinks.Contains("structure")) { return true; } - if (target is ItemPrefab && AllowedLinks.Contains("item")) { return true; } - if (target is LinkedSubmarinePrefab && Tags.Contains("dock")) { return true; } - if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock")) { return true; } - return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(identifier) - || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); - } - - //a method that allows the GUIListBoxes to check through a delegate if the entityprefab is still selected - public static object GetSelected() - { - return (object)selected; + if (target is StructurePrefab && AllowedLinks.Contains("structure".ToIdentifier())) { return true; } + if (target is ItemPrefab && AllowedLinks.Contains("item".ToIdentifier())) { return true; } + if (target is LinkedSubmarinePrefab && Tags.Contains("dock".ToIdentifier())) { return true; } + if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock".ToIdentifier())) { return true; } + return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(Identifier) + || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs index 8f9d7b13d..ba0c54911 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs @@ -1,233 +1,209 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; +using System.Linq; using Barotrauma.IO; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Xml.Linq; -using System.Linq; namespace Barotrauma { public class Md5Hash { - private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private const string cachePath = "Data/hashcache.txt"; - private static readonly Dictionary> cache = new Dictionary>(); - - public static void LoadCache() + public static class Cache { - try + private const string cachePath = "Data/hashcache.txt"; + + private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries + = new List<(string Path, Md5Hash Hash, DateTime DateTime)>(); + + public static void Load() { if (!File.Exists(cachePath)) { return; } - string[] lines = File.ReadAllLines(cachePath, Encoding.UTF8); - if (lines.Length <= 0 || lines[0] != GameMain.Version.ToString()) { return; } - foreach (string line in lines.Skip(1)) + var lines = File.ReadAllLines(cachePath); + if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version) { - if (string.IsNullOrWhiteSpace(line)) { continue; } - string[] parts = line.Split('|'); - if (parts.Length < 3) { continue; } - - string path = parts[0].CleanUpPath(); - string hashStr = parts[1]; - long timeLong = long.Parse(parts[2]); - - Md5Hash hash = new Md5Hash(hashStr); - DateTime time = DateTime.FromBinary(timeLong); - - if (File.GetLastWriteTime(path) == time && !cache.ContainsKey(path)) + for (int i = 1; i < lines.Length; i++) { - cache.Add(path, new Tuple(hash, timeLong)); + string[] split = lines[i].Split('|'); + string path = split[0].CleanUpPathCrossPlatform(); + Md5Hash hash = Md5Hash.StringAsHash(split[1]); + DateTime? dateTime = null; + if (long.TryParse(split[2], out long dateTimeUlong)) + { + dateTime = DateTime.FromBinary(dateTimeUlong); + } + + if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path)) + { + Entries.Add((path, hash, dateTime.Value)); + } } } } - catch (Exception e) + + public static void Add(string path, Md5Hash hash, DateTime dateTime) { - DebugConsole.NewMessage($"Failed to load hash cache: {e.Message}\n{e.StackTrace.CleanupStackTrace()}", Microsoft.Xna.Framework.Color.Orange); - cache.Clear(); + path = path.CleanUpPathCrossPlatform(); + Remove(path); + Entries.Add((path, hash, dateTime)); + } + + public static void Remove(string path) + { + path = path.CleanUpPathCrossPlatform(); + Entries.RemoveAll(e => e.Path == path); } } - public static void SaveCache() - { -#if SERVER - //don't save to the cache if the server is owned by a client, - //since this suggests that they're running concurrently and - //will interfere with each other here - if (GameMain.Server?.OwnerConnection != null) { return; } -#endif + public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); + + private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + //thanks to Jlobblet for this regex + private static readonly Regex stringHashRegex = new Regex(@"^[0-9a-fA-F]{7,32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - string[] lines = new string[cache.Count + 1]; - lines[0] = GameMain.Version.ToString(); - int i = 1; - foreach (KeyValuePair> kpv in cache) - { - lines[i] = kpv.Key + "|" + kpv.Value.Item1 + "|" + kpv.Value.Item2; - i++; - } - File.WriteAllLines(cachePath, lines, Encoding.UTF8); - } + public readonly byte[] ByteRepresentation; + public readonly string StringRepresentation; + public readonly string ShortRepresentation; - private bool LoadFromCache(string filename) - { - if (!string.IsNullOrWhiteSpace(filename)) - { - filename = filename.CleanUpPath(); - lock (cache) - { - if (cache.ContainsKey(filename)) - { - Hash = cache[filename].Item1.Hash; - ShortHash = cache[filename].Item1.ShortHash; - return true; - } - } - } - return false; - } - - public void SaveToCache(string filename, long? time = null) - { - if (string.IsNullOrWhiteSpace(filename)) { return; } - - lock (cache) - { - filename = filename.CleanUpPath(); - Tuple cacheVal = new Tuple(this, time ?? File.GetLastWriteTime(filename).ToBinary()); - if (cache.ContainsKey(filename)) - { - cache[filename] = cacheVal; - } - else - { - cache.Add(filename, cacheVal); - } - SaveCache(); - } - } - - public static Md5Hash FetchFromCache(string filename) - { - Md5Hash newHash = new Md5Hash(); - if (newHash.LoadFromCache(filename)) { return newHash; } - return null; - } - - public string Hash { get; private set; } - - public string ShortHash { get; private set; } - - private Md5Hash() - { - this.Hash = null; - ShortHash = null; - } - - public Md5Hash(string md5Hash) - { - this.Hash = md5Hash; - ShortHash = GetShortHash(md5Hash); - } - - public Md5Hash(byte[] bytes) - { - Hash = CalculateHash(bytes); - - ShortHash = GetShortHash(Hash); - } - - public Md5Hash(FileStream fileStream, string filename = null, bool tryLoadFromCache = true) - { - if (tryLoadFromCache) - { - if (LoadFromCache(filename)) { return; } - } - - Hash = CalculateHash(fileStream); - - ShortHash = GetShortHash(Hash); - - SaveToCache(filename); - } - - public Md5Hash(XDocument doc, string filename = null, bool tryLoadFromCache = true) - { - if (tryLoadFromCache) - { - if (LoadFromCache(filename)) { return; } - } - - if (doc == null) { return; } - - string docString = removeWhitespaceRegex.Replace(doc.ToString(), ""); - - byte[] inputBytes = Encoding.ASCII.GetBytes(docString); - - Hash = CalculateHash(inputBytes); - ShortHash = GetShortHash(Hash); - - SaveToCache(filename); - } - - public override string ToString() - { - return Hash; - } - - private string CalculateHash(FileStream stream) - { - using (MD5 md5 = MD5.Create()) - { - byte[] byteHash = md5.ComputeHash(stream); - // step 2, convert byte array to hex string - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < byteHash.Length; i++) - { - sb.Append(byteHash[i].ToString("X2")); - } - - return sb.ToString(); - } - } - - private string CalculateHash(byte[] bytes) + private static void CalculateHash(byte[] bytes, out string stringRepresentation, out byte[] byteRepresentation) { using (MD5 md5 = MD5.Create()) { byte[] byteHash = md5.ComputeHash(bytes); - // step 2, convert byte array to hex string - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < byteHash.Length; i++) - { - sb.Append(byteHash[i].ToString("X2")); - } - return sb.ToString(); + byteRepresentation = byteHash; + stringRepresentation = ByteRepresentationToStringRepresentation(byteHash); } } + private static string ByteRepresentationToStringRepresentation(byte[] byteHash) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < byteHash.Length; i++) + { + sb.Append(byteHash[i].ToString("X2")); + } + + return sb.ToString(); + } + + private static byte[] StringRepresentationToByteRepresentation(string strHash) + { + var byteRepresentation = new byte[strHash.Length / 2]; + for (int i = 0; i < byteRepresentation.Length; i++) + { + byteRepresentation[i] = Convert.ToByte(strHash.Substring(i * 2, 2), 16); + } + + return byteRepresentation; + } + public static string GetShortHash(string fullHash) { - if (string.IsNullOrEmpty(fullHash)) { return ""; } return fullHash.Length < 7 ? fullHash : fullHash.Substring(0, 7); } - public static bool RemoveFromCache(string filename) + private Md5Hash(string md5Hash) { - if (!string.IsNullOrWhiteSpace(filename)) + StringRepresentation = md5Hash; + ByteRepresentation = StringRepresentationToByteRepresentation(StringRepresentation); + + ShortRepresentation = GetShortHash(md5Hash); + } + + private Md5Hash(byte[] bytes, bool calculate) + { + if (calculate) { - filename = filename.CleanUpPath(); - lock (cache) - { - if (cache.ContainsKey(filename)) - { - cache.Remove(filename); - return true; - } - } + CalculateHash(bytes, out StringRepresentation, out ByteRepresentation); + } + else + { + StringRepresentation = ByteRepresentationToStringRepresentation(bytes); + ByteRepresentation = bytes; + } + + ShortRepresentation = GetShortHash(StringRepresentation); + } + + public static Md5Hash StringAsHash(string hash) + { + if (!stringHashRegex.IsMatch(hash)) { throw new ArgumentException($"{hash} is not a valid hash"); } + return new Md5Hash(hash); + } + + public static Md5Hash CalculateForBytes(byte[] bytes) + { + return new Md5Hash(bytes, calculate: true); + } + + public static Md5Hash BytesAsHash(byte[] bytes) + { + return new Md5Hash(bytes, calculate: false); + } + + [Flags] + public enum StringHashOptions + { + BytePerfect = 0, + IgnoreCase = 0x1, + IgnoreWhitespace = 0x2 + } + + public static Md5Hash CalculateForFile(string path, StringHashOptions options) + { + if (options.HasFlag(StringHashOptions.IgnoreWhitespace) || options.HasFlag(StringHashOptions.IgnoreCase)) + { + string str = File.ReadAllText(path, Encoding.UTF8); + return CalculateForString(str, options); + } + else + { + byte[] bytes = File.ReadAllBytes(path); + return CalculateForBytes(bytes); + } + } + + public static Md5Hash CalculateForString(string str, StringHashOptions options) + { + if (options.HasFlag(StringHashOptions.IgnoreCase)) + { + str = str.ToLowerInvariant(); + } + if (options.HasFlag(StringHashOptions.IgnoreWhitespace)) + { + str = removeWhitespaceRegex.Replace(str, ""); + } + byte[] bytes = Encoding.UTF8.GetBytes(str); + return CalculateForBytes(bytes); + } + + public override string ToString() + { + return StringRepresentation; + } + + public override bool Equals(object? obj) + { + if (obj is Md5Hash { StringRepresentation: { } otherStr }) + { + string selfStr = otherStr.Length < StringRepresentation.Length + ? StringRepresentation[..otherStr.Length] + : StringRepresentation; + otherStr = StringRepresentation.Length < otherStr.Length + ? otherStr[..StringRepresentation.Length] + : otherStr; + return selfStr.Equals(otherStr, StringComparison.OrdinalIgnoreCase); } return false; } + + public static bool operator ==(Md5Hash? a, Md5Hash? b) + => (a is null == b is null) && (a?.Equals(b) ?? true); + + public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 4e76b70b0..5397af405 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -1,93 +1,37 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma { - internal class NPCSet : IDisposable + internal class NPCSet : Prefab { - private static List? Sets { get; set; } + public readonly static PrefabCollection Sets = new PrefabCollection(); - private string Identifier { get; } - private readonly List Humans = new List(); + private readonly ImmutableArray Humans; private bool Disposed { get; set; } - private NPCSet(XElement element, string filePath) + public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - Identifier = element.GetAttributeString("identifier", string.Empty); - - foreach (XElement npcElement in element.Elements()) - { - Humans.Add(new HumanPrefab(npcElement, filePath)); - } + Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file)).ToImmutableArray(); } - public static HumanPrefab? Get(string identifier, string npcidentifier) + public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier) { - HumanPrefab prefab = Sets.Where(set => set.Identifier == identifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault(); + HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault(); if (prefab == null) { - DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{identifier}\"."); + DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); return null; } - return new HumanPrefab(prefab.Element, prefab.FilePath); - } - - public static void LoadSets() - { - Sets?.ForEach(set => set.Dispose()); - Sets = new List(); - IEnumerable files = GameMain.Instance.GetFilesOfType(ContentType.NPCSets); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - XElement? rootElement = doc?.Root; - - if (doc == null || rootElement == null) { continue; } - - if (doc.Root.IsOverride()) - { - Sets.Clear(); - DebugConsole.NewMessage($"Overriding all NPC sets with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in rootElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the NPC set config '{elementName}' in file '{file.Path}'"); - continue; - } - - var existingParams = Sets.Find(set => set.Identifier == identifier); - if (existingParams != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding NPC set config '{identifier}' using the file '{file.Path}'", Color.Yellow); - Sets.Remove(existingParams); - } - else - { - DebugConsole.ThrowError($"Duplicate NPC set config: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - - Sets.Add(new NPCSet(element, file.Path)); - } - } + return prefab; } private void Dispose(bool disposing) @@ -103,7 +47,7 @@ namespace Barotrauma Disposed = true; } - public void Dispose() + public override void Dispose() { Dispose(true); GC.SuppressFinalize(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 7b29db634..3ceac779c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -1,164 +1,209 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - class OutpostGenerationParams : ISerializableEntity + class OutpostGenerationParams : PrefabWithUintIdentifier, ISerializableEntity { - public static List Params { get; private set; } - + public readonly static PrefabCollection OutpostParams = new PrefabCollection(); + public virtual string Name { get; private set; } - - public string Identifier { get; private set; } - - private readonly List allowedLocationTypes = new List(); + + private readonly HashSet allowedLocationTypes = new HashSet(); /// /// Identifiers of the location types this outpost can appear in. If empty, can appear in all types of locations. /// - public IEnumerable AllowedLocationTypes + public IEnumerable AllowedLocationTypes { get { return allowedLocationTypes; } } - [Serialize(10, isSaveable: true), Editable(MinValueInt = 1, MaxValueInt = 50)] + [Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount { get; set; } - [Serialize(200.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float MinHallwayLength { get; set; } - [Serialize(false, isSaveable: true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AlwaysDestructible { get; set; } - [Serialize(false, isSaveable: true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AlwaysRewireable { get; set; } - [Serialize(false, isSaveable: true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AllowStealing { get; set; } - [Serialize(true, isSaveable: true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool SpawnCrewInsideOutpost { get; set; } - - [Serialize(true, isSaveable: true), Editable] + + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool LockUnusedDoors { get; set; } - [Serialize(true, isSaveable: true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool RemoveUnusedGaps { get; set; } - [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinWaterPercentage { get; set; } - [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MaxWaterPercentage { get; set; } - [Serialize("", isSaveable: true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } - private readonly Dictionary moduleCounts = new Dictionary(); + private readonly Dictionary moduleCounts = new Dictionary(); - public IEnumerable> ModuleCounts + public IReadOnlyDictionary ModuleCounts { get { return moduleCounts; } } - private readonly List> humanPrefabLists = new List>(); - - public Dictionary SerializableProperties { get; private set; } - - protected OutpostGenerationParams(XElement element, string filePath) + private class NpcCollection : IReadOnlyList { - Identifier = element.GetAttributeString("identifier", ""); - Name = element.GetAttributeString("name", Identifier); - allowedLocationTypes = element.GetAttributeStringArray("allowedlocationtypes", Array.Empty()).ToList(); - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + private class Entry + { + private readonly HumanPrefab humanPrefab = null; + private readonly Identifier setIdentifier = Identifier.Empty; + private readonly Identifier npcIdentifier = Identifier.Empty; + + public Entry(HumanPrefab humanPrefab) + { + this.humanPrefab = humanPrefab; + } - if (element == null) { return; } - foreach (XElement subElement in element.Elements()) + public Entry(Identifier setIdentifier, Identifier npcIdentifier) + { + this.setIdentifier = setIdentifier; + this.npcIdentifier = npcIdentifier; + } + + public HumanPrefab HumanPrefab + => humanPrefab ?? NPCSet.Get(setIdentifier, npcIdentifier); + } + + private readonly List entries = new List(); + + public void Add(HumanPrefab humanPrefab) + => entries.Add(new Entry(humanPrefab)); + + + public void Add(Identifier setIdentifier, Identifier npcIdentifier) + => entries.Add(new Entry(setIdentifier, npcIdentifier)); + + public IEnumerator GetEnumerator() + { + foreach (var entry in entries) + { + yield return entry.HumanPrefab; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => entries.Count; + + public HumanPrefab this[int index] => entries[index].HumanPrefab; + } + + private readonly ImmutableArray> humanPrefabCollections; + + public Dictionary SerializableProperties { get; private set; } + + #warning TODO: this shouldn't really accept any ContentFile, issue is that RuinConfigFile and OutpostConfigFile are separate derived classes + public OutpostGenerationParams(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + Name = element.GetAttributeString("name", Identifier.Value); + allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty()).ToHashSet(); + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + var humanPrefabCollections = new List>(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "modulecount": - string moduleFlag = (subElement.GetAttributeString("flag", null) ?? subElement.GetAttributeString("moduletype", "")).ToLowerInvariant(); + Identifier moduleFlag = subElement.GetAttributeIdentifier("flag", subElement.GetAttributeIdentifier("moduletype", "")); moduleCounts[moduleFlag] = subElement.GetAttributeInt("count", 0); break; case "npcs": - humanPrefabLists.Add(new List()); - foreach (XElement npcElement in subElement.Elements()) + var newCollection = new NpcCollection(); + foreach (var npcElement in subElement.Elements()) { - string from = npcElement.GetAttributeString("from", string.Empty); - - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (!string.IsNullOrWhiteSpace(from)) + Identifier from = npcElement.GetAttributeIdentifier("from", Identifier.Empty); + + if (from != Identifier.Empty) { - HumanPrefab prefab = NPCSet.Get(from, npcElement.GetAttributeString("identifier", string.Empty)); - if (prefab != null) - { - humanPrefabLists.Last().Add(prefab); - } + newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty)); } else { - humanPrefabLists.Last().Add(new HumanPrefab(npcElement, filePath)); + newCollection.Add(new HumanPrefab(npcElement, file)); } } + humanPrefabCollections.Add(newCollection); break; } } + + this.humanPrefabCollections = humanPrefabCollections.ToImmutableArray(); } - public int GetModuleCount(string moduleFlag) + public int GetModuleCount(Identifier moduleFlag) { - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return int.MaxValue; } + if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return int.MaxValue; } return moduleCounts.ContainsKey(moduleFlag) ? moduleCounts[moduleFlag] : 0; } - public void SetModuleCount(string moduleFlag, int count) + public void SetModuleCount(Identifier moduleFlag, int count) { - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return; } + if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; } if (count <= 0) { moduleCounts.Remove(moduleFlag); @@ -169,66 +214,22 @@ namespace Barotrauma } } - public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) + public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) { this.allowedLocationTypes.Clear(); - foreach (string locationType in allowedLocationTypes) + foreach (Identifier locationType in allowedLocationTypes) { - if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; } + if (locationType == "any") { continue; } this.allowedLocationTypes.Add(locationType); } } - public IEnumerable GetHumanPrefabs(Rand.RandSync randSync) + public IReadOnlyList GetHumanPrefabs(Rand.RandSync randSync) { - if (humanPrefabLists == null || !humanPrefabLists.Any()) { return Enumerable.Empty(); } - return humanPrefabLists.GetRandom(randSync); + if (!humanPrefabCollections.Any()) { return Array.Empty(); } + return humanPrefabCollections.GetRandom(randSync); } - public static void LoadPresets() - { - Params = new List(); - var files = GameMain.Instance.GetFilesOfType(ContentType.OutpostConfig); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - Params.Clear(); - DebugConsole.NewMessage($"Overriding all outpost generation parameters with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the outpost config '{elementName}' in file '{file.Path}'"); - continue; - } - var existingParams = Params.Find(p => p.Identifier == identifier); - if (existingParams != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding outpost config '{identifier}' using the file '{file.Path}'", Color.Yellow); - Params.Remove(existingParams); - } - else - { - DebugConsole.ThrowError($"Duplicate outpost config: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - Params.Add(new OutpostGenerationParams(element, file.Path)); - } - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 39b5f6bf1..8a92048f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -3,7 +3,9 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Security.Cryptography; namespace Barotrauma { @@ -26,7 +28,7 @@ namespace Barotrauma public OutpostModuleInfo.GapPosition UsedGapPositions = 0; - public readonly HashSet FulfilledModuleTypes = new HashSet(); + public readonly HashSet FulfilledModuleTypes = new HashSet(); public Vector2 Offset; @@ -67,10 +69,18 @@ namespace Barotrauma private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) { - var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule); + var outpostModuleFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToArray(); + var uintIdDupes = outpostModuleFiles.Where(f1 => + outpostModuleFiles.Any(f2 => f1 != f2 && f1.UintIdentifier == f2.UintIdentifier)).ToArray(); + if (uintIdDupes.Any()) + { + throw new Exception($"OutpostModuleFile UintIdentifier duplicates found: {uintIdDupes.Select(f => f.Path)}"); + } if (location != null) { - if (location.IsCriticallyRadiated() && OutpostGenerationParams.Params.FirstOrDefault(p => p.Identifier.Equals(generationParams.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newParams) + if (location.IsCriticallyRadiated() && OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.Identifier == generationParams.ReplaceInRadiation) is { } newParams) { generationParams = newParams; } @@ -80,21 +90,21 @@ namespace Barotrauma //load the infos of the outpost module files List outpostModules = new List(); - foreach (ContentFile outpostModuleFile in outpostModuleFiles) + foreach (var outpostModuleFile in outpostModuleFiles) { - var subInfo = new SubmarineInfo(outpostModuleFile.Path); + var subInfo = new SubmarineInfo(outpostModuleFile.Path.Value); if (subInfo.OutpostModuleInfo != null) { if (generationParams is RuinGeneration.RuinGenerationParams) { //if the module doesn't have the ruin flag or any other flag used in the generation params, don't use it in ruins - if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin") && + if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) && !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key))) { continue; } } - else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin")) + else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier())) { continue; } @@ -131,14 +141,23 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - var pendingModuleFlags = onlyEntrance ? new List() { generationParams.ModuleCounts.First().Key } : SelectModules(outpostModules, generationParams); - foreach (string flag in pendingModuleFlags.Distinct().ToList()) + List pendingModuleFlags; + using (var md5 = MD5.Create()) { - if (flag.Equals("none", StringComparison.OrdinalIgnoreCase)) { continue; } + #warning TODO: cursed + pendingModuleFlags = onlyEntrance + ? generationParams.ModuleCounts + .Keys.OrderBy(k => ToolBox.IdentifierToUint32Hash(k, md5)) + .First().ToEnumerable().ToList() + : SelectModules(outpostModules, generationParams); + } + foreach (Identifier flag in pendingModuleFlags) + { + if (flag == "none") { continue; } int pendingCount = pendingModuleFlags.Count(f => f == flag); int availableModuleCount = outpostModules - .Where(m => m.OutpostModuleInfo.ModuleFlags.Any(f => f.Equals(flag, StringComparison.OrdinalIgnoreCase))) + .Where(m => m.OutpostModuleInfo.ModuleFlags.Any(f => f == flag)) .Select(m => m.OutpostModuleInfo.MaxCount) .DefaultIfEmpty(0) .Sum(); @@ -154,7 +173,7 @@ namespace Barotrauma } //the first module is spawned separately, remove it from the list of pending modules - string initialModuleFlag = pendingModuleFlags.FirstOrDefault() ?? "airlock"; + Identifier initialModuleFlag = pendingModuleFlags.FirstOrDefault().IfEmpty("airlock".ToIdentifier()); pendingModuleFlags.Remove(initialModuleFlag); var initialModule = GetRandomModule(outpostModules, initialModuleFlag, locationType); @@ -162,9 +181,9 @@ namespace Barotrauma { throw new Exception("Failed to generate an outpost (no airlock modules found)."); } - foreach (string initialFlag in initialModule.OutpostModuleInfo.ModuleFlags) + foreach (Identifier initialFlag in initialModule.OutpostModuleInfo.ModuleFlags) { - if (pendingModuleFlags.Contains("initialFlag")) { pendingModuleFlags.Remove(initialFlag); } + if (pendingModuleFlags.Contains("initialFlag".ToIdentifier())) { pendingModuleFlags.Remove(initialFlag); } } if (remainingTries == 1) @@ -176,7 +195,7 @@ namespace Barotrauma selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None)); selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag); AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); - if (pendingModuleFlags.Any(flag => !flag.Equals("none", StringComparison.OrdinalIgnoreCase))) + if (pendingModuleFlags.Any(flag => flag != "none")) { remainingTries--; if (remainingTries <= 0) @@ -196,7 +215,7 @@ namespace Barotrauma sub.Info.OutpostGenerationParams = generationParams; if (!generationFailed) { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != sub) { continue; } if (string.IsNullOrEmpty(hull.RoomName) || @@ -223,12 +242,14 @@ namespace Barotrauma DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); - var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost); + var outpostFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToArray(); if (!outpostFiles.Any()) { throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available."); } - var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.Server).Path) + var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.ServerAndClient).Path.Value) { Type = SubmarineType.Outpost }; @@ -386,7 +407,7 @@ namespace Barotrauma } else { - hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.Server) * 0.01f; + hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.ServerAndClient) * 0.01f; } } } @@ -400,13 +421,13 @@ namespace Barotrauma /// /// Select the number and types of the modules to use in the outpost /// - private static List SelectModules(IEnumerable modules, OutpostGenerationParams generationParams) + private static List SelectModules(IEnumerable modules, OutpostGenerationParams generationParams) { int totalModuleCount = generationParams.TotalModuleCount; - var pendingModuleFlags = new List(); + var pendingModuleFlags = new List(); bool availableModulesFound = true; - string initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key; + Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key; pendingModuleFlags.Add(initialModuleFlag); while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { @@ -426,13 +447,17 @@ namespace Barotrauma pendingModuleFlags.Add(moduleFlag.Key); } } - pendingModuleFlags.Shuffle(Rand.RandSync.Server); + using (MD5 md5 = MD5.Create()) + { + pendingModuleFlags.Sort((i1, i2) => (int)ToolBox.StringToUInt32Hash(i1.Value.ToLowerInvariant(), md5) - (int)ToolBox.StringToUInt32Hash(i2.Value.ToLowerInvariant(), md5)); + } + pendingModuleFlags.Shuffle(Rand.RandSync.ServerAndClient); while (pendingModuleFlags.Count < totalModuleCount) { //don't place "none" modules at the end because // a. "filler rooms" at the end of a hallway are pointless // b. placing the unnecessary filler rooms first give more options for the placement of the more important modules - pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.Server), "none"); + pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.ServerAndClient), "none".ToIdentifier()); } //make sure the initial module is inserted first @@ -452,7 +477,7 @@ namespace Barotrauma /// The modules we've already selected to be used in the outpost. private static bool AppendToModule(PlacedModule currentModule, List availableModules, - List pendingModuleFlags, + List pendingModuleFlags, List selectedModules, LocationType locationType, bool retry = true, @@ -461,7 +486,7 @@ namespace Barotrauma if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions().Randomize(Rand.RandSync.Server)) + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } if (!allowExtendBelowInitialModule) @@ -526,15 +551,15 @@ namespace Barotrauma PlacedModule currentModule, OutpostModuleInfo.GapPosition gapPosition, List availableModules, - List pendingModuleFlags, + List pendingModuleFlags, List selectedModules, LocationType locationType) { if (pendingModuleFlags.Count == 0) { return null; } - string flagToPlace = "none"; + Identifier flagToPlace = "none".ToIdentifier(); SubmarineInfo nextModule = null; - foreach (string moduleFlag in pendingModuleFlags) + foreach (Identifier moduleFlag in pendingModuleFlags) { flagToPlace = moduleFlag; nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType); @@ -547,7 +572,7 @@ namespace Barotrauma { Offset = currentModule.Offset + GetMoveDir(gapPosition), }; - foreach (string moduleFlag in nextModule.OutpostModuleInfo.ModuleFlags) + foreach (Identifier moduleFlag in nextModule.OutpostModuleInfo.ModuleFlags) { if (!pendingModuleFlags.Contains(moduleFlag)) { continue; } if (moduleFlag != "none" || flagToPlace == "none") @@ -711,19 +736,19 @@ namespace Barotrauma return solutionFound; } - private static SubmarineInfo GetRandomModule(IEnumerable modules, string moduleFlag, LocationType locationType) + private static SubmarineInfo GetRandomModule(IEnumerable modules, Identifier moduleFlag, LocationType locationType) { IEnumerable availableModules = null; - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag.Equals("none")) + if (moduleFlag.IsEmpty || moduleFlag == "none") { - availableModules = modules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || m.OutpostModuleInfo.ModuleFlags.Contains("none")); + availableModules = modules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())); } else { availableModules = modules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); if (moduleFlag != "hallwayhorizontal" && moduleFlag != "hallwayvertical") { - availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal") && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical")); + availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal".ToIdentifier()) && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical".ToIdentifier())); } } @@ -731,7 +756,7 @@ namespace Barotrauma //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type if (!modulesSuitableForLocationType.Any()) @@ -742,21 +767,21 @@ namespace Barotrauma if (!modulesSuitableForLocationType.Any()) { DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); - return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server); + return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } else { - return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server); + return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } } - private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, string moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) + private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) { IEnumerable availableModules = null; - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag.Equals("none")) + if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { availableModules = modules - .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none")) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); } else { @@ -772,7 +797,7 @@ namespace Barotrauma //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type if (!modulesSuitableForLocationType.Any()) @@ -783,11 +808,11 @@ namespace Barotrauma if (!modulesSuitableForLocationType.Any()) { DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); - return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server); + return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } else { - return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server); + return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } } @@ -807,13 +832,13 @@ namespace Barotrauma } } - private static IEnumerable GapPositions() + private readonly static OutpostModuleInfo.GapPosition[] GapPositions = new[] { - yield return OutpostModuleInfo.GapPosition.Right; - yield return OutpostModuleInfo.GapPosition.Left; - yield return OutpostModuleInfo.GapPosition.Top; - yield return OutpostModuleInfo.GapPosition.Bottom; - } + OutpostModuleInfo.GapPosition.Right, + OutpostModuleInfo.GapPosition.Left, + OutpostModuleInfo.GapPosition.Top, + OutpostModuleInfo.GapPosition.Bottom + }; private static OutpostModuleInfo.GapPosition GetOpposingGapPosition(OutpostModuleInfo.GapPosition thisGapPosition) { @@ -824,7 +849,7 @@ namespace Barotrauma OutpostModuleInfo.GapPosition.Bottom => OutpostModuleInfo.GapPosition.Top, OutpostModuleInfo.GapPosition.Top => OutpostModuleInfo.GapPosition.Bottom, OutpostModuleInfo.GapPosition.None => OutpostModuleInfo.GapPosition.None, - _ => throw new InvalidOperationException() + _ => throw new ArgumentException() }; } @@ -837,7 +862,7 @@ namespace Barotrauma OutpostModuleInfo.GapPosition.Bottom => Vector2.UnitY, OutpostModuleInfo.GapPosition.Top => -Vector2.UnitY, OutpostModuleInfo.GapPosition.None => Vector2.Zero, - _ => throw new InvalidOperationException() + _ => throw new ArgumentException() }; } @@ -885,7 +910,7 @@ namespace Barotrauma private static bool CanAttachTo(OutpostModuleInfo from, OutpostModuleInfo to) { - if (!from.AllowAttachToModules.Any() || from.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))) { return true; } + if (!from.AllowAttachToModules.Any() || from.AllowAttachToModules.All(s => s == "any")) { return true; } return from.AllowAttachToModules.Any(s => to.ModuleFlags.Contains(s)); } @@ -915,7 +940,7 @@ namespace Barotrauma { for (int i = 0; i < thisJunctionBox.Connections.Count && i < previousJunctionBox.Connections.Count; i++) { - var wirePrefab = MapEntityPrefab.Find(name: null, identifier: thisJunctionBox.Connections[i].IsPower ? "redwire" : "bluewire") as ItemPrefab; + var wirePrefab = MapEntityPrefab.FindByIdentifier((thisJunctionBox.Connections[i].IsPower ? "redwire" : "bluewire").ToIdentifier()) as ItemPrefab; var wire = new Item(wirePrefab, thisJunctionBox.Item.Position, sub).GetComponent(); if (!thisJunctionBox.Connections[i].TryAddLink(wire)) @@ -1021,9 +1046,9 @@ namespace Barotrauma { suitableModules = availableModules.Where(m => !m.OutpostModuleInfo.AllowAttachToModules.Any() || - m.OutpostModuleInfo.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))); + m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")); } - var hallwayInfo = GetRandomModule(suitableModules, isHorizontal ? "hallwayhorizontal" : "hallwayvertical", locationType); + var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType); if (hallwayInfo == null) { DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\"."); @@ -1442,50 +1467,47 @@ namespace Barotrauma if (outpost?.Info?.OutpostGenerationParams == null) { return; } List killedCharacters = new List(); - Dictionary selectedCharacters = new Dictionary(); - foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.Server)) + List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters + = new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>(); + foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient)) { - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient); if (location != null && location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { killedCharacters.Add(humanPrefab); continue; } - selectedCharacters.Add(humanPrefab, characterInfo); + selectedCharacters.Add((humanPrefab, characterInfo)); } //replace killed characters with new ones foreach (HumanPrefab killedCharacter in killedCharacters) { - int tries = 0; - while (tries < 100) + for (int tries = 0; tries < 100; tries++) { - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient); if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { - selectedCharacters.Add(killedCharacter, characterInfo); + selectedCharacters.Add((killedCharacter, characterInfo)); break; } } } - foreach (var selectedCharacter in selectedCharacters) + foreach ((var humanPrefab, var characterInfo) in selectedCharacters) { - HumanPrefab humanPrefab = selectedCharacter.Key; - CharacterInfo characterInfo = selectedCharacter.Value; - Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name)); ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags()); if (gotoTarget == null) { - gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.Server); + gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.ServerAndClient); } characterInfo.TeamID = CharacterTeamType.FriendlyNPC; - var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + var npc = Character.Create(CharacterPrefab.HumanSpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; - npc.Prefab = humanPrefab; + npc.HumanPrefab = humanPrefab; if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) { outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); @@ -1499,7 +1521,7 @@ namespace Barotrauma { npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier); } - humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server); + humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index c0c051586..c79a10a64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -18,46 +18,46 @@ namespace Barotrauma Bottom = 8 } - private readonly HashSet moduleFlags = new HashSet(); - public IEnumerable ModuleFlags + private readonly HashSet moduleFlags = new HashSet(); + public IEnumerable ModuleFlags { get { return moduleFlags; } } - private readonly HashSet allowAttachToModules = new HashSet(); - public IEnumerable AllowAttachToModules + private readonly HashSet allowAttachToModules = new HashSet(); + public IEnumerable AllowAttachToModules { get { return allowAttachToModules; } } - private readonly HashSet allowedLocationTypes = new HashSet(); - public IEnumerable AllowedLocationTypes + private readonly HashSet allowedLocationTypes = new HashSet(); + public IEnumerable AllowedLocationTypes { get { return allowedLocationTypes; } } - [Serialize(100, isSaveable: true, description: "How many instances of this module can be used in one outpost."), Editable] + [Serialize(100, IsPropertySaveable.Yes, description: "How many instances of this module can be used in one outpost."), Editable] public int MaxCount { get; set; } - [Serialize(10.0f, isSaveable: true, description: "How likely it is for the module to get picked when selecting from a set of modules during the outpost generation."), Editable] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How likely it is for the module to get picked when selecting from a set of modules during the outpost generation."), Editable] public float Commonness { get; set; } - [Serialize(GapPosition.None, isSaveable: true, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] + [Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] public GapPosition GapPositions { get; set; } public string Name { get; private set; } - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } public OutpostModuleInfo(SubmarineInfo submarineInfo, XElement element) { Name = $"OutpostModuleInfo ({submarineInfo.Name})"; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); SetFlags( - element.GetAttributeStringArray("flags", null, convertToLowerInvariant: true) ?? - element.GetAttributeStringArray("moduletypes", new string[0], convertToLowerInvariant: true)); - SetAllowAttachTo(element.GetAttributeStringArray("allowattachto", new string[0], convertToLowerInvariant: true)); - allowedLocationTypes = new HashSet(element.GetAttributeStringArray("allowedlocationtypes", new string[0], convertToLowerInvariant: true)); + element.GetAttributeIdentifierArray("flags", null) ?? + element.GetAttributeIdentifierArray("moduletypes", Array.Empty())); + SetAllowAttachTo(element.GetAttributeIdentifierArray("allowattachto", Array.Empty())); + allowedLocationTypes = new HashSet(element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty())); } public OutpostModuleInfo(SubmarineInfo submarineInfo) @@ -68,12 +68,12 @@ namespace Barotrauma public OutpostModuleInfo(OutpostModuleInfo original) { Name = original.Name; - moduleFlags = new HashSet(original.moduleFlags); - allowAttachToModules = new HashSet(original.allowAttachToModules); - allowedLocationTypes = new HashSet(original.allowedLocationTypes); - SerializableProperties = new Dictionary(); + moduleFlags = new HashSet(original.moduleFlags); + allowAttachToModules = new HashSet(original.allowAttachToModules); + allowedLocationTypes = new HashSet(original.allowedLocationTypes); + SerializableProperties = new Dictionary(); GapPositions = original.GapPositions; - foreach (KeyValuePair kvp in original.SerializableProperties) + foreach (KeyValuePair kvp in original.SerializableProperties) { SerializableProperties.Add(kvp.Key, kvp.Value); if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null) @@ -83,51 +83,51 @@ namespace Barotrauma } } - public void SetFlags(IEnumerable newFlags) + public void SetFlags(IEnumerable newFlags) { moduleFlags.Clear(); - if (newFlags.Contains("hallwayhorizontal")) + if (newFlags.Contains("hallwayhorizontal".ToIdentifier())) { - moduleFlags.Add("hallwayhorizontal"); - if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } + moduleFlags.Add("hallwayhorizontal".ToIdentifier()); + if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } return; } - if (newFlags.Contains("hallwayvertical")) + if (newFlags.Contains("hallwayvertical".ToIdentifier())) { - moduleFlags.Add("hallwayvertical"); - if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } + moduleFlags.Add("hallwayvertical".ToIdentifier()); + if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } return; } if (!newFlags.Any()) { - moduleFlags.Add("none"); + moduleFlags.Add("none".ToIdentifier()); } - foreach (string flag in newFlags) + foreach (Identifier flag in newFlags) { if (flag == "none" && newFlags.Count() > 1) { continue; } - moduleFlags.Add(flag.ToLowerInvariant()); + moduleFlags.Add(flag); } } - public void SetAllowAttachTo(IEnumerable allowAttachTo) + public void SetAllowAttachTo(IEnumerable allowAttachTo) { allowAttachToModules.Clear(); if (!allowAttachTo.Any()) { - allowAttachToModules.Add("any"); + allowAttachToModules.Add("any".ToIdentifier()); } - foreach (string flag in allowAttachTo) + foreach (Identifier flag in allowAttachTo) { if (flag == "any" && allowAttachTo.Count() > 1) { continue; } allowAttachToModules.Add(flag); } } - public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) + public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) { this.allowedLocationTypes.Clear(); - foreach (string locationType in allowedLocationTypes) + foreach (Identifier locationType in allowedLocationTypes) { - if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; } + if (locationType == "any") { continue; } this.allowedLocationTypes.Add(locationType); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 25dff440e..0037d0a9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -28,6 +28,7 @@ namespace Barotrauma /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. /// public readonly float BuyingPriceMultiplier = 1f; + public bool DisplayNonEmpty { get; } = false; /// @@ -48,7 +49,7 @@ namespace Barotrauma MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f) + public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, bool displayNonEmpty = false) { Price = price; CanBeBought = canBeBought; @@ -58,38 +59,41 @@ namespace Barotrauma MaxAvailableAmount = Math.Max(maxAmount, minAmount); MinLevelDifficulty = minLevelDifficulty; CanBeSpecial = canBeSpecial; + DisplayNonEmpty = displayNonEmpty; } - public static List> CreatePriceInfos(XElement element, out PriceInfo defaultPrice) + public static List> CreatePriceInfos(XElement element, out PriceInfo defaultPrice) { defaultPrice = null; - var basePrice = element.GetAttributeInt("baseprice", 0); - var soldByDefault = element.GetAttributeBool("soldbydefault", true); - var minAmount = GetMinAmount(element); - var maxAmount = GetMaxAmount(element); - var minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); - var canBeSpecial = element.GetAttributeBool("canbespecial", true); - var buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); - var priceInfos = new List>(); + int basePrice = element.GetAttributeInt("baseprice", 0); + bool soldByDefault = element.GetAttributeBool("soldbydefault", true); + int minAmount = GetMinAmount(element); + int maxAmount = GetMaxAmount(element); + int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); + bool canBeSpecial = element.GetAttributeBool("canbespecial", true); + float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); + bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false); + var priceInfos = new List>(); foreach (XElement childElement in element.GetChildElements("price")) { - var priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); - var sold = childElement.GetAttributeBool("sold", soldByDefault); - priceInfos.Add(new Tuple(childElement.GetAttributeString("locationtype", "").ToLowerInvariant(), - new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold, - minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, - maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, + float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); + bool sold = childElement.GetAttributeBool("sold", soldByDefault); + priceInfos.Add(new Tuple(childElement.GetAttributeIdentifier("locationtype", ""), + new PriceInfo((int)(priceMultiplier * basePrice), sold, + sold ? GetMinAmount(childElement, minAmount) : 0, + sold ? GetMaxAmount(childElement, maxAmount) : 0, canBeSpecial, - childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier)))); + childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), + childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier), + displayNonEmpty))); } - var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); + bool canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations, - minAmount: canBeBoughtAtOtherLocations ? minAmount : 0, - maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0, - canBeSpecial, - minLevelDifficulty, buyingPriceMultiplier); + canBeBoughtAtOtherLocations ? minAmount : 0, + canBeBoughtAtOtherLocations ? maxAmount : 0, + canBeSpecial, minLevelDifficulty, buyingPriceMultiplier, displayNonEmpty); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index c68763505..7556b281a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; using Barotrauma.Abilities; #if CLIENT using Microsoft.Xna.Framework.Graphics; @@ -56,9 +57,9 @@ namespace Barotrauma private readonly List bodyDebugDimensions = new List(); #if DEBUG - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] #else - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] #endif public bool Indestructible { @@ -75,7 +76,7 @@ namespace Barotrauma public override Sprite Sprite { - get { return prefab.sprite; } + get { return base.Prefab.Sprite; } } public bool IsPlatform @@ -91,7 +92,7 @@ namespace Barotrauma public override string Name { - get { return prefab.Name; } + get { return base.Prefab.Name.Value; } } public bool HasBody @@ -115,7 +116,7 @@ namespace Barotrauma private float? maxHealth; - [Serialize(100.0f, true), Editable(MinValueFloat = 0)] + [Serialize(100.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0)] public float MaxHealth { get => maxHealth ?? Prefab.Health; @@ -124,7 +125,7 @@ namespace Barotrauma private float crushDepth; - [Serialize(Level.DefaultRealWorldCrushDepth, true)] + [Serialize(Level.DefaultRealWorldCrushDepth, IsPropertySaveable.Yes)] public float CrushDepth { get => crushDepth; @@ -163,29 +164,26 @@ namespace Barotrauma private set; } - public StructurePrefab Prefab => prefab as StructurePrefab; + public new StructurePrefab Prefab => base.Prefab as StructurePrefab; - public HashSet Tags - { - get { return prefab.Tags; } - } + public ImmutableHashSet Tags => Prefab.Tags; protected Color spriteColor; - [Editable, Serialize("1.0,1.0,1.0,1.0", true)] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)] public Color SpriteColor { get { return spriteColor; } set { spriteColor = value; } } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool UseDropShadow { get; private set; } - [Editable, Serialize("0,0", true, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")] + [Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")] public Vector2 DropShadowOffset { get; @@ -201,7 +199,7 @@ namespace Barotrauma if (scale == value) { return; } scale = MathHelper.Clamp(value, 0.1f, 10.0f); - float relativeScale = scale / prefab.Scale; + float relativeScale = scale / base.Prefab.Scale; if (!ResizeHorizontal || !ResizeVertical) { @@ -230,7 +228,7 @@ namespace Barotrauma protected Vector2 textureScale = Vector2.One; - [Editable(DecimalCount = 3, MinValueFloat = 0.01f, MaxValueFloat = 10f, ValueStep = 0.1f), Serialize("1.0, 1.0", false)] + [Editable(DecimalCount = 3, MinValueFloat = 0.01f, MaxValueFloat = 10f, ValueStep = 0.1f), Serialize("1.0, 1.0", IsPropertySaveable.No)] public Vector2 TextureScale { get { return textureScale; } @@ -250,7 +248,7 @@ namespace Barotrauma } protected Vector2 textureOffset = Vector2.Zero; - [Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", true)] + [Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", IsPropertySaveable.Yes)] public Vector2 TextureOffset { get { return textureOffset; } @@ -343,14 +341,14 @@ namespace Barotrauma } } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool NoAITarget { get; private set; } - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -406,7 +404,7 @@ namespace Barotrauma rect = rectangle; TextureScale = sp.TextureScale; - spriteColor = prefab.SpriteColor; + spriteColor = base.Prefab.SpriteColor; if (sp.IsHorizontal.HasValue) { IsHorizontal = sp.IsHorizontal.Value; @@ -461,7 +459,7 @@ namespace Barotrauma SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this); #if CLIENT - foreach (XElement subElement in sp.ConfigElement.Elements()) + foreach (var subElement in sp.ConfigElement.Elements()) { if (subElement.Name.ToString().Equals("light", StringComparison.OrdinalIgnoreCase)) { @@ -524,7 +522,7 @@ namespace Barotrauma { defaultRect = defaultRect }; - foreach (KeyValuePair property in SerializableProperties) + foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) { continue; } clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); @@ -572,13 +570,13 @@ namespace Barotrauma { if (FlippedX && IsHorizontal) { - xsections = (int)Math.Ceiling((float)rect.Width / prefab.sprite.SourceRect.Width); - width = prefab.sprite.SourceRect.Width; + xsections = (int)Math.Ceiling((float)rect.Width / base.Prefab.Sprite.SourceRect.Width); + width = base.Prefab.Sprite.SourceRect.Width; } else if (FlippedY && !IsHorizontal) { - ysections = (int)Math.Ceiling((float)rect.Height / prefab.sprite.SourceRect.Height); - width = prefab.sprite.SourceRect.Height; + ysections = (int)Math.Ceiling((float)rect.Height / base.Prefab.Sprite.SourceRect.Height); + width = base.Prefab.Sprite.SourceRect.Height; } else { @@ -1184,7 +1182,7 @@ namespace Barotrauma { if (damageDiff < 0.0f) { - attacker.Info?.IncreaseSkillLevel("mechanical", + attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(), -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f)); } } @@ -1374,10 +1372,10 @@ namespace Barotrauma } } - public static Structure Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { string name = element.Attribute("name").Value; - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = FindPrefab(name, identifier); if (prefab == null) @@ -1398,7 +1396,7 @@ namespace Barotrauma } bool hasDamage = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1421,7 +1419,7 @@ namespace Barotrauma break; case "upgrade": { - var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty); + var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier); int level = subElement.GetAttributeInt("level", 1); if (upgradePrefab != null) @@ -1460,18 +1458,18 @@ namespace Barotrauma return s; } - public static StructurePrefab FindPrefab(string name, string identifier) + public static StructurePrefab FindPrefab(string name, Identifier identifier) { StructurePrefab prefab = null; - if (string.IsNullOrEmpty(identifier)) + if (identifier.IsEmpty) { //legacy support: //1. attempt to find a prefab with an empty identifier and a matching name prefab = MapEntityPrefab.Find(name, "") as StructurePrefab; //2. not found, attempt to find a prefab with a matching name - if (prefab == null) prefab = MapEntityPrefab.Find(name) as StructurePrefab; + if (prefab == null) { prefab = MapEntityPrefab.Find(name) as StructurePrefab; } //3. not found, attempt to find a prefab that uses the previous name as an identifier - if (prefab == null) prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; + if (prefab == null) { prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; } } else { @@ -1488,8 +1486,8 @@ namespace Barotrauma int height = ResizeVertical ? rect.Height : defaultRect.Height; element.Add( - new XAttribute("name", prefab.Name), - new XAttribute("identifier", prefab.Identifier), + new XAttribute("name", base.Prefab.Name), + new XAttribute("identifier", base.Prefab.Identifier), new XAttribute("ID", ID), new XAttribute("rect", (int)(rect.X - Submarine.HiddenSubPosition.X) + "," + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 805bc42b8..91da0bd7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.IO; +using System.Collections.Immutable; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -15,169 +16,98 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override LocalizedString Name { get; } - private string name; - public override string Name - { - get { return name; } - } + public readonly ContentXElement ConfigElement; - public XElement ConfigElement { get; private set; } - - private bool canSpriteFlipX, canSpriteFlipY; - - private float health; - - //default size - private Vector2 size; - - //does the structure have a physics body - [Serialize(false, false)] - public bool Body - { - get; - private set; - } - - //rotation of the physics body in degrees - [Serialize(0.0f, false)] - public float BodyRotation - { - get; - private set; - } - - //in display units - [Serialize(0.0f, false)] - public float BodyWidth - { - get; - private set; - } - - //in display units - [Serialize(0.0f, false)] - public float BodyHeight - { - get; - private set; - } - - //in display units - [Serialize("0.0,0.0", false)] - public Vector2 BodyOffset - { - get; - private set; - } - - [Serialize(false, false)] - public bool Platform - { - get; - private set; - } - - [Serialize(false, false)] - public bool AllowAttachItems - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float MinHealth - { - get; - set; - } - - [Serialize(100.0f, false)] - public float Health - { - get { return health; } - set { health = Math.Max(value, MinHealth); } - } - - [Serialize(true, false)] - public bool IndestructibleInOutposts - { - get; - set; - } - - [Serialize(false, false)] - public bool CastShadow - { - get; - private set; - } + public readonly bool CanSpriteFlipX; + public readonly bool CanSpriteFlipY; /// /// If null, the orientation is determined automatically based on the dimensions of the structure instances /// - public bool? IsHorizontal + public readonly bool? IsHorizontal; + + public Vector2 ScaledSize => Size * Scale; + + public readonly Sprite BackgroundSprite; + + public override Sprite Sprite { get; } + + public override string OriginalName => Name.Value; + + public override ImmutableHashSet Tags { get; } + + public override ImmutableHashSet AllowedLinks { get; } + + public override MapEntityCategory Category { get; } + + public override ImmutableHashSet Aliases { get; } + + //does the structure have a physics body + [Serialize(false, IsPropertySaveable.No)] + public bool Body { get; private set; } + + //rotation of the physics body in degrees + [Serialize(0.0f, IsPropertySaveable.No)] + public float BodyRotation { get; private set; } + + //in display units + [Serialize(0.0f, IsPropertySaveable.No)] + public float BodyWidth { get; private set; } + + //in display units + [Serialize(0.0f, IsPropertySaveable.No)] + public float BodyHeight { get; private set; } + + //in display units + [Serialize("0.0,0.0", IsPropertySaveable.No)] + public Vector2 BodyOffset { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool Platform { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowAttachItems { get; private set; } + + [Serialize(0.0f, IsPropertySaveable.No)] + public float MinHealth { get; private set; } + + private float health; + [Serialize(100.0f, IsPropertySaveable.No)] + public float Health { - get; - private set; + get { return health; } + private set { health = Math.Max(value, MinHealth); } } - [Serialize(Direction.None, false)] - public Direction StairDirection - { - get; - private set; - } + [Serialize(true, IsPropertySaveable.No)] + public bool IndestructibleInOutposts { get; private set; } - [Serialize(45.0f, false)] - public float StairAngle - { - get; - private set; - } + [Serialize(false, IsPropertySaveable.No)] + public bool CastShadow { get; private set; } - [Serialize(false, false)] - public bool NoAITarget - { - get; - private set; - } + [Serialize(Direction.None, IsPropertySaveable.No)] + public Direction StairDirection { get; private set; } - public bool CanSpriteFlipX - { - get { return canSpriteFlipX; } - } + [Serialize(45.0f, IsPropertySaveable.No)] + public float StairAngle { get; private set; } - public bool CanSpriteFlipY - { - get { return canSpriteFlipY; } - } + [Serialize(false, IsPropertySaveable.No)] + public bool NoAITarget { get; private set; } - [Serialize("0,0", true)] - public Vector2 Size - { - get { return size; } - private set { size = value; } - } + [Serialize("0,0", IsPropertySaveable.Yes)] + public Vector2 Size { get; private set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string DamageSound { get; private set; } - public Vector2 ScaledSize => size * Scale; - protected Vector2 textureScale = Vector2.One; - [Editable(DecimalCount = 3), Serialize("1.0, 1.0", true)] + [Editable(DecimalCount = 3), Serialize("1.0, 1.0", IsPropertySaveable.Yes)] public Vector2 TextureScale { get { return textureScale; } - set + private set { textureScale = new Vector2( MathHelper.Clamp(value.X, 0.01f, 10), @@ -185,155 +115,112 @@ namespace Barotrauma } } - public Sprite BackgroundSprite + protected override Identifier DetermineIdentifier(XElement element) { - get; - private set; - } - - public static void LoadAll(IEnumerable files) - { - foreach (ContentFile file in files) + Identifier identifier = base.DetermineIdentifier(element); + string originalName = element.GetAttributeString("name", ""); + if (identifier.IsEmpty && !string.IsNullOrEmpty(originalName)) { - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var rootElement = doc.Root; - if (rootElement.IsOverride()) - { - foreach (var element in rootElement.Elements()) + string categoryStr = element.GetAttributeString("category", "Misc"); + if (Enum.TryParse(categoryStr, true, out MapEntityCategory category) && category.HasFlag(MapEntityCategory.Legacy)) { - foreach (var childElement in element.Elements()) - { - Load(childElement, true, file); - } - } - } - else - { - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - foreach (var childElement in element.Elements()) - { - Load(childElement, true, file); - } - } - else - { - Load(element, false, file); - } + identifier = $"legacystructure_{originalName.Replace(" ", "")}".ToIdentifier(); } } + return identifier; } - public static void RemoveByFile(string filePath) + public StructurePrefab(ContentXElement element, StructureFile file) : base(element, file) { - Prefabs.RemoveByFile(filePath); - } + Name = element.GetAttributeString("name", ""); + ConfigElement = element; - private static StructurePrefab Load(XElement element, bool allowOverride, ContentFile file) - { - StructurePrefab sp = new StructurePrefab - { - originalName = element.GetAttributeString("name", ""), - FilePath = file.Path, - ContentPackage = file.ContentPackage - }; - sp.name = sp.originalName; - sp.ConfigElement = element; - sp.identifier = element.GetAttributeString("identifier", ""); - - var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty; - - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + var parentType = element.Parent?.GetAttributeIdentifier("prefabtype", Identifier.Empty) ?? Identifier.Empty; + + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); //only used if the item doesn't have a name/description defined in the currently selected language - string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); + Identifier fallbackNameIdentifier = element.GetAttributeIdentifier("fallbacknameidentifier", ""); - string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); + Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); - if (string.IsNullOrEmpty(sp.originalName)) + if (Name.IsNullOrEmpty()) { - if (string.IsNullOrEmpty(nameIdentifier)) + Name = TextManager.Get($"EntityName.{Identifier}"); + if (!nameIdentifier.IsEmpty) { - sp.name = TextManager.Get("EntityName." + sp.identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; + Name = TextManager.Get($"EntityName.{nameIdentifier}").Fallback(Name); } - else + + if (!fallbackNameIdentifier.IsEmpty) { - sp.name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; + Name = Name.Fallback(TextManager.Get($"EntityName.{fallbackNameIdentifier}")); } } - if (string.IsNullOrEmpty(sp.name)) - { - sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})"; - } - sp.Tags = new HashSet(); + var tags = new HashSet(); string joinedTags = element.GetAttributeString("tags", ""); if (string.IsNullOrEmpty(joinedTags)) joinedTags = element.GetAttributeString("Tags", ""); foreach (string tag in joinedTags.Split(',')) { - sp.Tags.Add(tag.Trim().ToLowerInvariant()); + tags.Add(tag.Trim().ToIdentifier()); } if (element.Attribute("ishorizontal") != null) { - sp.IsHorizontal = element.GetAttributeBool("ishorizontal", false); + IsHorizontal = element.GetAttributeBool("ishorizontal", false); } - foreach (XElement subElement in element.Elements()) +#if CLIENT + var decorativeSprites = new List(); + var decorativeSpriteGroups = new Dictionary>(); +#endif + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString()) { case "sprite": - sp.sprite = new Sprite(subElement, lazyLoad: true); + Sprite = new Sprite(subElement, lazyLoad: true); if (subElement.Attribute("sourcerect") == null && subElement.Attribute("sheetindex") == null) { - DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!"); + DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!"); } #if CLIENT if (subElement.GetAttributeBool("fliphorizontal", false)) - sp.sprite.effects = SpriteEffects.FlipHorizontally; + Sprite.effects = SpriteEffects.FlipHorizontally; if (subElement.GetAttributeBool("flipvertical", false)) - sp.sprite.effects = SpriteEffects.FlipVertically; + Sprite.effects = SpriteEffects.FlipVertically; #endif - sp.canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); - sp.canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); + CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); + CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); - if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(sp.Name)) + if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) { - sp.sprite.Name = sp.Name; + Sprite.Name = Name.Value; } - sp.sprite.EntityID = sp.identifier; + Sprite.EntityIdentifier = Identifier; break; case "backgroundsprite": - sp.BackgroundSprite = new Sprite(subElement, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && sp.sprite != null) + BackgroundSprite = new Sprite(subElement, lazyLoad: true); + if (subElement.Attribute("sourcerect") == null && Sprite != null) { - sp.BackgroundSprite.SourceRect = sp.sprite.SourceRect; - sp.BackgroundSprite.size = sp.sprite.size; - sp.BackgroundSprite.size.X *= sp.sprite.SourceRect.Width; - sp.BackgroundSprite.size.Y *= sp.sprite.SourceRect.Height; - sp.BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); + BackgroundSprite.SourceRect = Sprite.SourceRect; + BackgroundSprite.size = Sprite.size; + BackgroundSprite.size.X *= Sprite.SourceRect.Width; + BackgroundSprite.size.Y *= Sprite.SourceRect.Height; + BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); } #if CLIENT - if (subElement.GetAttributeBool("fliphorizontal", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; } - if (subElement.GetAttributeBool("flipvertical", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; } - sp.BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White); + if (subElement.GetAttributeBool("fliphorizontal", false)) { BackgroundSprite.effects = SpriteEffects.FlipHorizontally; } + if (subElement.GetAttributeBool("flipvertical", false)) { BackgroundSprite.effects = SpriteEffects.FlipVertically; } + BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White); #endif break; case "decorativesprite": #if CLIENT string decorativeSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) + if (subElement.DoesAttributeReferenceFileNameAlone("texture")) { decorativeSpriteFolder = Path.GetDirectoryName(file.Path); } @@ -347,101 +234,111 @@ namespace Barotrauma else { decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); - sp.DecorativeSprites.Add(decorativeSprite); + decorativeSprites.Add(decorativeSprite); groupID = decorativeSprite.RandomGroupID; } - if (!sp.DecorativeSpriteGroups.ContainsKey(groupID)) + if (!decorativeSpriteGroups.ContainsKey(groupID)) { - sp.DecorativeSpriteGroups.Add(groupID, new List()); + decorativeSpriteGroups.Add(groupID, new List()); } - sp.DecorativeSpriteGroups[groupID].Add(decorativeSprite); + decorativeSpriteGroups[groupID].Add(decorativeSprite); #endif break; } } - - if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) +#if CLIENT + DecorativeSprites = decorativeSprites.ToImmutableArray(); + DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); +#endif + + if (parentType == "wrecked") { - if (!string.IsNullOrEmpty(sp.Name)) + if (!Name.IsNullOrEmpty()) { - sp.name = TextManager.GetWithVariable("wreckeditemformat", "[name]", sp.name); + Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name); } } string categoryStr = element.GetAttributeString("category", "Structure"); if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { - category = MapEntityCategory.Structure; + category = MapEntityCategory.Structure; } - sp.Category = category; + Category = category; - if (category.HasFlag(MapEntityCategory.Legacy)) - { - if (string.IsNullOrWhiteSpace(sp.identifier)) - { - sp.identifier = "legacystructure_" + sp.name.ToLowerInvariant().Replace(" ", ""); - } - } - - sp.Aliases = - (element.GetAttributeStringArray("aliases", null) ?? - element.GetAttributeStringArray("Aliases", new string[0])).ToHashSet(); + Aliases = + (element.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? + element.GetAttributeStringArray("Aliases", Array.Empty(), convertToLowerInvariant: true)).ToImmutableHashSet(); string nonTranslatedName = element.GetAttributeString("name", null) ?? element.Name.ToString(); - sp.Aliases.Add(nonTranslatedName.ToLowerInvariant()); + Aliases.Add(nonTranslatedName.ToLowerInvariant()); - SerializableProperty.DeserializeProperties(sp, element); - if (sp.Body) + SerializableProperty.DeserializeProperties(this, element); + if (Body) { - sp.Tags.Add("wall"); + tags.Add("wall".ToIdentifier()); } - if (string.IsNullOrEmpty(sp.Description)) + if (Description.IsNullOrEmpty()) { - if (!string.IsNullOrEmpty(descriptionIdentifier)) + if (!descriptionIdentifier.IsEmpty) { - sp.Description = TextManager.Get("EntityDescription." + descriptionIdentifier, returnNull: true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); } - else if (string.IsNullOrEmpty(nameIdentifier)) + else if (nameIdentifier.IsEmpty) { - sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{Identifier}"); } else { - sp.Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); } } //backwards compatibility if (element.Attribute("size") == null) { - sp.size = Vector2.Zero; + Size = Vector2.Zero; if (element.Attribute("width") == null && element.Attribute("height") == null) { - sp.size.X = sp.sprite.SourceRect.Width; - sp.size.Y = sp.sprite.SourceRect.Height; + Size = Sprite.SourceRect.Size.ToVector2(); } else { - sp.size.X = element.GetAttributeFloat("width", 0.0f); - sp.size.Y = element.GetAttributeFloat("height", 0.0f); + Size = new Vector2( + element.GetAttributeFloat("width", 0.0f), + element.GetAttributeFloat("height", 0.0f)); } } //backwards compatibility if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) { - sp.Category = MapEntityCategory.Wrecked; - sp.Subcategory = "Thalamus"; + Category = MapEntityCategory.Wrecked; + Subcategory = "Thalamus"; } - if (string.IsNullOrEmpty(sp.identifier)) + if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - "Structure prefab \"" + sp.name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + "Structure prefab \"" + Name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); } - Prefabs.Add(sp, allowOverride); - return sp; + + Tags = tags.ToImmutableHashSet(); + AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); + } + + protected override void CreateInstance(Rectangle rect) + { + throw new NotImplementedException(); + } + + private bool disposed = false; + public override void Dispose() + { + if (disposed) { return; } + disposed = true; + Prefabs.Remove(this); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 6901ddf25..1ba321562 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -291,7 +291,7 @@ namespace Barotrauma public int CalculateBasePrice() { int minPrice = 1000; - float volume = Hull.hullList.Where(h => h.Submarine == this).Sum(h => h.Volume); + float volume = Hull.HullList.Where(h => h.Submarine == this).Sum(h => h.Volume); float itemValue = Item.ItemList.Where(it => it.Submarine == this).Sum(it => it.Prefab.GetMinPrice() ?? 0); float price = volume / 500.0f + itemValue / 100.0f; System.Diagnostics.Debug.Assert(price >= 0); @@ -300,7 +300,7 @@ namespace Barotrauma private float ballastFloraTimer; public bool ImmuneToBallastFlora { get; set; } - public void AttemptBallastFloraInfection(string identifier, float deltaTime, float probability) + public void AttemptBallastFloraInfection(Identifier identifier, float deltaTime, float probability) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (ImmuneToBallastFlora) { return; } @@ -557,7 +557,7 @@ namespace Barotrauma public Rectangle CalculateDimensions(bool onlyHulls = true) { List entities = onlyHulls ? - Hull.hullList.FindAll(h => h.Submarine == this).Cast().ToList() : + Hull.HullList.FindAll(h => h.Submarine == this).Cast().ToList() : MapEntity.mapEntityList.FindAll(me => me.Submarine == this); //ignore items whose body is disabled (wires, items inside cabinets) @@ -990,7 +990,7 @@ namespace Barotrauma { if (e.Submarine == this) { - Spawner.AddToRemoveQueue(e); + Spawner.AddEntityToRemoveQueue(e); } } @@ -1114,7 +1114,7 @@ namespace Barotrauma float waterVolume = 0.0f; float volume = 0.0f; float excessWater = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != this) { continue; } waterVolume += hull.WaterVolume; @@ -1212,7 +1212,7 @@ namespace Barotrauma /// public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub); - public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.hullList); + public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.HullList); public List GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList); public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList); public List GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList); @@ -1285,7 +1285,7 @@ namespace Barotrauma if (element.Name != "Structure") { continue; } string name = element.GetAttributeString("name", ""); - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = Structure.FindPrefab(name, identifier); if (prefab == null || !prefab.Body) { continue; } @@ -1348,7 +1348,7 @@ namespace Barotrauma } Vector2 center = Vector2.Zero; - var matchingHulls = Hull.hullList.FindAll(h => h.Submarine == this); + var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this); if (matchingHulls.Any()) { @@ -1538,7 +1538,16 @@ namespace Barotrauma Rectangle dimensions = VisibleBorders; element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); var cargoContainers = GetCargoContainers(); - element.Add(new XAttribute("cargocapacity", cargoContainers.Sum(c => c.container.Capacity))); + int cargoCapacity = cargoContainers.Sum(c => c.container.Capacity); + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + { + cargoCapacity += linkedSub.CargoCapacity; + } + } + + element.Add(new XAttribute("cargocapacity", cargoCapacity)); element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); @@ -1654,7 +1663,7 @@ namespace Barotrauma Unloading = true; #if CLIENT - RemoveAllRoundSounds(); + RoundSound.RemoveAllRoundSounds(); GameMain.LightManager?.ClearLights(); #endif @@ -1697,6 +1706,8 @@ namespace Barotrauma GameMain.World?.Clear(); GameMain.World = null; + Powered.Grids.Clear(); + GC.Collect(); Unloading = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 499acf25d..3216589a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -118,7 +118,7 @@ namespace Barotrauma Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero; Body farseerBody = null; - if (!Hull.hullList.Any(h => h.Submarine == sub)) + if (!Hull.HullList.Any(h => h.Submarine == sub)) { farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f); if (showWarningMessages) @@ -156,7 +156,7 @@ namespace Barotrauma } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != submarine || hull.IdFreed) { continue; } @@ -446,7 +446,7 @@ namespace Barotrauma { float waterVolume = 0.0f; float volume = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != submarine) continue; @@ -850,7 +850,7 @@ namespace Barotrauma { errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; } - if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "SubmarineBody.ApplyImpact:InvalidImpulse", GameAnalyticsManager.ErrorSeverity.Error, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 1ec236c0a..cc6881f34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -31,10 +31,7 @@ namespace Barotrauma public const string SavePath = "Submarines"; private static List savedSubmarines = new List(); - public static IEnumerable SavedSubmarines - { - get { return savedSubmarines; } - } + public static IEnumerable SavedSubmarines => savedSubmarines; private Task hashTask; private Md5Hash hash; @@ -59,13 +56,13 @@ namespace Barotrauma set; } - public string DisplayName + public LocalizedString DisplayName { get; set; } - public string Description + public LocalizedString Description { get; set; @@ -170,7 +167,7 @@ namespace Barotrauma get { if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } - return RequiredContentPackages.All(cp => GameMain.Config.AllEnabledPackages.Any(cp2 => cp2.Name == cp)); + return RequiredContentPackages.All(reqName => ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.NameMatches(reqName))); } set { @@ -199,13 +196,14 @@ namespace Barotrauma public OutpostGenerationParams OutpostGenerationParams; - public readonly Dictionary> OutpostNPCs = new Dictionary>(); + public readonly Dictionary> OutpostNPCs = new Dictionary>(); //constructors & generation ---------------------------------------------------- public SubmarineInfo() { FilePath = null; - Name = DisplayName = TextManager.Get("UnspecifiedSubFileName"); + DisplayName = TextManager.Get("UnspecifiedSubFileName"); + Name = DisplayName.Value; IsFileCorrupted = false; RequiredContentPackages = new HashSet(); } @@ -219,7 +217,8 @@ namespace Barotrauma } try { - Name = DisplayName = Path.GetFileNameWithoutExtension(filePath); + DisplayName = Path.GetFileNameWithoutExtension(filePath); + Name = DisplayName.Value; } catch (Exception e) { @@ -228,7 +227,7 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(hash)) { - this.hash = new Md5Hash(hash); + this.hash = Md5Hash.StringAsHash(hash); } IsFileCorrupted = false; @@ -314,11 +313,9 @@ namespace Barotrauma private void Init() { - DisplayName = TextManager.Get("Submarine.Name." + Name, true); - if (string.IsNullOrEmpty(DisplayName)) { DisplayName = Name; } + DisplayName = TextManager.Get("Submarine.Name." + Name).Fallback(Name); - Description = TextManager.Get("Submarine.Description." + Name, true); - if (string.IsNullOrEmpty(Description)) { Description = SubmarineElement.GetAttributeString("description", ""); } + Description = TextManager.Get("Submarine.Description." + Name).Fallback(SubmarineElement.GetAttributeString("description", "")); EqualityCheckVal = SubmarineElement.GetAttributeInt("checkval", 0); @@ -379,7 +376,7 @@ namespace Barotrauma } RequiredContentPackages.Clear(); - string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", new string[0]); + string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty()); foreach (string contentPackageName in contentPackageNames) { RequiredContentPackages.Add(contentPackageName); @@ -405,14 +402,9 @@ namespace Barotrauma var vanilla = GameMain.VanillaContent; if (vanilla != null) { - var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine) - .Concat(vanilla.GetFilesOfType(ContentType.Wreck)) - .Concat(vanilla.GetFilesOfType(ContentType.BeaconStation)) - .Concat(vanilla.GetFilesOfType(ContentType.EnemySubmarine)) - .Concat(vanilla.GetFilesOfType(ContentType.Outpost)) - .Concat(vanilla.GetFilesOfType(ContentType.OutpostModule)); - string pathToCompare = FilePath.Replace(@"\", @"/").ToLowerInvariant(); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) + var vanillaSubs = vanilla.GetFiles(); + string pathToCompare = FilePath.CleanUpPath(); + if (vanillaSubs.Any(sub => sub.Path == pathToCompare)) { return true; } @@ -427,7 +419,8 @@ namespace Barotrauma hashTask = new Task(() => { - hash = new Md5Hash(doc, FilePath); + hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace); + Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow); }); hashTask.Start(); } @@ -459,7 +452,7 @@ namespace Barotrauma LeftBehindSubDockingPortOccupied = false; LeftBehindDockingPortIDs.Clear(); BlockedDockingPortIDs.Clear(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("linkedsubmarine", StringComparison.OrdinalIgnoreCase)) { continue; } if (subElement.Attribute("location") == null) { continue; } @@ -469,7 +462,7 @@ namespace Barotrauma LeftBehindDockingPortIDs.Add(targetDockingPortID); XElement targetPortElement = targetDockingPortID == 0 ? null : element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); - if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) + if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", Array.Empty()).Length > 0) { BlockedDockingPortIDs.Add(targetDockingPortID); LeftBehindSubDockingPortOccupied = true; @@ -488,7 +481,7 @@ namespace Barotrauma foreach (var structureElement in SubmarineElement.GetChildElements("structure")) { string name = structureElement.Attribute("name")?.Value ?? ""; - string identifier = structureElement.GetAttributeString("identifier", ""); + Identifier identifier = structureElement.GetAttributeIdentifier("identifier", ""); var structurePrefab = Structure.FindPrefab(name, identifier); if (structurePrefab == null || !structurePrefab.Body) { continue; } if (!structureCrushDepthsDefined && structureElement.Attribute("crushdepth") != null) @@ -546,7 +539,7 @@ namespace Barotrauma } SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.RemoveFromCache(filePath); + Md5Hash.Cache.Remove(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) @@ -578,10 +571,7 @@ namespace Barotrauma public static void RefreshSavedSubs() { - var contentPackageSubs = ContentPackage.GetFilesOfType( - GameMain.Config.AllEnabledPackages, - ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, - ContentType.Wreck, ContentType.BeaconStation, ContentType.EnemySubmarine); + var contentPackageSubs = ContentPackageManager.EnabledPackages.All.SelectMany(c => c.GetFiles()); for (int i = savedSubmarines.Count - 1; i >= 0; i--) { @@ -589,7 +579,7 @@ namespace Barotrauma { bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath); - bool isInContentPackage = contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()); + bool isInContentPackage = contentPackageSubs.Any(f => f.Path == savedSubmarines[i].FilePath); if (isDownloadedSub) { continue; } if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; } } @@ -640,11 +630,11 @@ namespace Barotrauma } } - foreach (ContentFile subFile in contentPackageSubs) + foreach (BaseSubFile subFile in contentPackageSubs) { - if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path))) + if (!filePaths.Any(fp => fp == subFile.Path)) { - filePaths.Add(subFile.Path); + filePaths.Add(subFile.Path.Value); } } @@ -661,7 +651,7 @@ namespace Barotrauma TextManager.Get("Error"), TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" + TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); string filePath = path; deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 98b33bb6e..6b22e9acf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -27,7 +27,7 @@ namespace Barotrauma public Ladder Ladders; public Structure Stairs; - private List tags; + private HashSet tags; public bool isObstructed; @@ -78,10 +78,7 @@ namespace Barotrauma } } - public IEnumerable Tags - { - get { return tags; } - } + public IEnumerable Tags => tags; public JobPrefab AssignedJob { get; private set; } @@ -114,7 +111,7 @@ namespace Barotrauma public WayPoint(Rectangle newRect, Submarine submarine) - : this (MapEntityPrefab.Find(null, "waypoint"), newRect, submarine) + : this (MapEntityPrefab.FindByIdentifier("waypoint".ToIdentifier()), newRect, submarine) { } @@ -122,8 +119,8 @@ namespace Barotrauma : base (prefab, submarine, id) { rect = newRect; - idCardTags = new string[0]; - tags = new List(); + idCardTags = Array.Empty(); + tags = new HashSet(); #if CLIENT if (iconSprites == null) @@ -165,7 +162,7 @@ namespace Barotrauma public static bool GenerateSubWaypoints(Submarine submarine) { - if (!Hull.hullList.Any()) + if (!Hull.HullList.Any()) { DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found."); return false; @@ -189,13 +186,14 @@ namespace Barotrauma door.Body.Enabled = true; } } - bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin"); + bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()); float diffFromHullEdge = 50; float minDist = 100.0f; float heightFromFloor = 110.0f; float hullMinHeight = 100; + var removals = new HashSet(); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (isFlooded) { @@ -587,7 +585,7 @@ namespace Barotrauma Body pickedBody = Submarine.PickBody( ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, Physics.CollisionWall, false, - (Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent() != null); + (Fixture f) => f.Body.UserData is Item pickedItem && pickedItem.GetComponent() != null); Door pickedDoor = null; if (pickedBody != null) @@ -876,9 +874,9 @@ namespace Barotrauma return WayPointList.GetRandom(wp => (ignoreSubmarine || wp.Submarine == sub) && wp.spawnType == spawnType && - (string.IsNullOrEmpty(spawnPointTag) || wp.Tags.Any(t => t.Equals(spawnPointTag, StringComparison.OrdinalIgnoreCase))) && + (spawnPointTag.IsNullOrEmpty() || wp.Tags.Any(t => t == spawnPointTag)) && (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)), - useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); + useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced); } public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine) @@ -922,7 +920,7 @@ namespace Barotrauma var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.AssignedJob == null); if (nonJobSpecificPoints.Any()) { - assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)]; + assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.ServerAndClient)]; } if (assignedWayPoints[i] != null) { continue; } @@ -966,13 +964,9 @@ namespace Barotrauma { Stairs = null; Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs); - if (pickedBody != null && pickedBody.UserData is Structure) + if (pickedBody != null && pickedBody.UserData is Structure structure && structure.StairDirection != Direction.None) { - Structure structure = (Structure)pickedBody.UserData; - if (structure != null && structure.StairDirection != Direction.None) - { - Stairs = structure; - } + Stairs = structure; } } @@ -985,13 +979,12 @@ namespace Barotrauma } if (ladderId > 0) { - Item ladderItem = FindEntityByID(ladderId) as Item; - if (ladderItem != null) { Ladders = ladderItem.GetComponent(); } + if (FindEntityByID(ladderId) is Item ladderItem) { Ladders = ladderItem.GetComponent(); } ladderId = 0; } } - public static WayPoint Load(XElement element, Submarine submarine, IdRemap idRemap) + public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = new Rectangle( int.Parse(element.Attribute("x").Value), @@ -1000,8 +993,10 @@ namespace Barotrauma Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); - WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine, idRemap.GetOffsetId(element)); - w.spawnType = spawnType; + WayPoint w = new WayPoint(MapEntityPrefab.FindByIdentifier((spawnType == SpawnType.Path ? "waypoint" : "spawnpoint").ToIdentifier()), rect, submarine, idRemap.GetOffsetId(element)) + { + spawnType = spawnType + }; string idCardDescString = element.GetAttributeString("idcarddesc", ""); if (!string.IsNullOrWhiteSpace(idCardDescString)) @@ -1014,7 +1009,7 @@ namespace Barotrauma w.IdCardTags = idCardTagString.Split(','); } - w.tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true).ToList(); + w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(jobIdentifier)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 12ecc872f..b56eb93b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -56,9 +56,9 @@ namespace Barotrauma.Networking { if (Type.HasFlag(ChatMessageType.Server) || Type.HasFlag(ChatMessageType.Error) || Type.HasFlag(ChatMessageType.ServerLog)) { - if (translatedText == null || translatedText.Length == 0) + if (translatedText.IsNullOrEmpty()) { - translatedText = TextManager.GetServerMessage(Text); + translatedText = TextManager.GetServerMessage(Text).Value; } return translatedText; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 332de65d0..daf4b37c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -15,11 +15,11 @@ namespace Barotrauma.Networking public UInt64 SteamID; public UInt64 OwnerSteamID; - public string Language; + public LanguageIdentifier Language; public UInt16 Ping; - public string PreferredJob; + public Identifier PreferredJob; public CharacterTeamType TeamID; @@ -148,7 +148,7 @@ namespace Barotrauma.Networking private List kickVoters; - public HashSet GivenAchievements = new HashSet(); + public HashSet GivenAchievements = new HashSet(); public ClientPermissions Permissions = ClientPermissions.None; public List PermittedConsoleCommands diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 01ef58ee2..0c3b25d2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -33,16 +33,16 @@ namespace Barotrauma.Networking { public static List List = new List(); - public readonly string Name; - public readonly string Description; + public readonly LocalizedString Name; + public readonly LocalizedString Description; public readonly ClientPermissions Permissions; public readonly List PermittedCommands; public PermissionPreset(XElement element) { string name = element.GetAttributeString("name", ""); - Name = TextManager.Get("permissionpresetname." + name, true) ?? name; - Description = TextManager.Get("permissionpresetdescription." + name, true) ?? element.GetAttributeString("description", ""); + Name = TextManager.Get("permissionpresetname." + name).Fallback(name); + Description = TextManager.Get("permissionpresetdescription." + name) .Fallback(element.GetAttributeString("description", "")); string permissionsStr = element.GetAttributeString("permissions", ""); if (!Enum.TryParse(permissionsStr, out Permissions)) @@ -53,7 +53,7 @@ namespace Barotrauma.Networking PermittedCommands = new List(); if (Permissions.HasFlag(ClientPermissions.ConsoleCommands)) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = subElement.GetAttributeString("name", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 1e3495f4f..f411d9107 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -117,7 +117,7 @@ namespace Barotrauma class CharacterSpawnInfo : IEntitySpawnInfo { - public readonly string identifier; + public readonly Identifier Identifier; public readonly CharacterInfo CharacterInfo; public readonly Vector2 Position; @@ -125,30 +125,32 @@ namespace Barotrauma private readonly Action onSpawned; - public CharacterSpawnInfo(string identifier, Vector2 worldPosition, Action onSpawn = null) + public CharacterSpawnInfo(Identifier identifier, Vector2 worldPosition, Action onSpawn = null) { - this.identifier = identifier ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null."); + this.Identifier = identifier; + if (identifier.IsEmpty) { throw new ArgumentException($"{nameof(CharacterSpawnInfo)} identifier cannot be null."); } Position = worldPosition; this.onSpawned = onSpawn; } - public CharacterSpawnInfo(string identifier, Vector2 position, Submarine sub, Action onSpawn = null) + public CharacterSpawnInfo(Identifier identifier, Vector2 position, Submarine sub, Action onSpawn = null) { - this.identifier = identifier ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null."); + this.Identifier = identifier; + if (identifier.IsEmpty) { throw new ArgumentException($"{nameof(CharacterSpawnInfo)} identifier cannot be null."); } Position = position; Submarine = sub; this.onSpawned = onSpawn; } - public CharacterSpawnInfo(string identifier, Vector2 position, CharacterInfo characterInfo, Action onSpawn = null) : this (identifier, position, onSpawn) + public CharacterSpawnInfo(Identifier identifier, Vector2 position, CharacterInfo characterInfo, Action onSpawn = null) : this (identifier, position, onSpawn) { CharacterInfo = characterInfo; } public Entity Spawn() { - var character = string.IsNullOrEmpty(identifier) ? null : - Character.Create(identifier, + var character = Identifier.IsEmpty ? null : + Character.Create(Identifier, Submarine == null ? Position : Submarine.Position + Position, ToolBox.RandomSeed(8), CharacterInfo, createNetworkEvent: false); return character; @@ -199,6 +201,7 @@ namespace Barotrauma public readonly Entity Entity; public readonly UInt16 OriginalID, OriginalInventoryID; + public readonly int OriginalSlotIndex; public readonly byte OriginalItemContainerIndex; @@ -219,6 +222,7 @@ namespace Barotrauma if (entity is Item item && item.ParentInventory?.Owner != null) { OriginalInventoryID = item.ParentInventory.Owner.ID; + OriginalSlotIndex = item.ParentInventory.FindIndex(item); //find the index of the ItemContainer this item is inside to get the item to //spawn in the correct inventory in multi-inventory items like fabricators if (item.Container != null) @@ -250,7 +254,7 @@ namespace Barotrauma return "EntitySpawner"; } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition = null, int? quality = null, Action onSpawned = null) + public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition = null, int? quality = null, Action onSpawned = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -263,7 +267,7 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) + public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -276,7 +280,7 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) + public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -294,10 +298,10 @@ namespace Barotrauma }); } - public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, Action onSpawn = null) + public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action onSpawn = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (string.IsNullOrEmpty(speciesName)) + if (speciesName.IsEmpty) { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); @@ -307,10 +311,10 @@ namespace Barotrauma spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); } - public void AddToSpawnQueue(string speciesName, Vector2 position, Submarine sub, Action onSpawn = null) + public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action onSpawn = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (string.IsNullOrEmpty(speciesName)) + if (speciesName.IsEmpty) { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); @@ -320,10 +324,10 @@ namespace Barotrauma spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); } - public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) + public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (string.IsNullOrEmpty(speciesName)) + if (speciesName.IsEmpty) { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); @@ -333,10 +337,11 @@ namespace Barotrauma spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); } - public void AddToRemoveQueue(Entity entity) + public void AddEntityToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (removeQueue.Contains(entity) || entity.Removed || entity == null || entity.IdFreed) { return; } + if (entity is Item item) { AddItemToRemoveQueue(item); return; } if (entity is Character) { Character character = entity as Character; @@ -352,7 +357,7 @@ namespace Barotrauma removeQueue.Enqueue(entity); } - public void AddToRemoveQueue(Item item) + public void AddItemToRemoveQueue(Item item) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (removeQueue.Contains(item) || item.Removed) { return; } @@ -364,7 +369,7 @@ namespace Barotrauma { if (containedItem != null) { - AddToRemoveQueue(containedItem); + AddItemToRemoveQueue(containedItem); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs index a5d48f5eb..86c2600bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs @@ -12,6 +12,6 @@ enum FileTransferType { - Submarine, CampaignSave + Submarine, CampaignSave, Mod } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index a30e4476c..a1631780f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -77,6 +77,7 @@ namespace Barotrauma { typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) }, { typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDynamic) }, { typeof(String), new ReadWriteBehavior(ReadString, WriteDynamic) }, + { typeof(Identifier), new ReadWriteBehavior(ReadIdentifier, WriteDynamic) }, { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } }.ToImmutableDictionary(); @@ -86,7 +87,7 @@ namespace Barotrauma private static readonly ImmutableDictionary, ReadWriteBehavior> TypePredicates = new Dictionary, ReadWriteBehavior> { // Arrays - { type => type.BaseType?.IsAssignableFrom(typeof(Array)) ?? false, new ReadWriteBehavior(ReadArray, WriteArray) }, + { type => typeof(Array).IsAssignableFrom(type.BaseType), new ReadWriteBehavior(ReadArray, WriteArray) }, // Nested INetSerializableStructs { type => typeof(INetSerializableStruct).IsAssignableFrom(type), new ReadWriteBehavior(ReadINetSerializableStruct, WriteINetSerializableStruct) }, @@ -94,14 +95,77 @@ namespace Barotrauma // Enums { type => type.IsEnum, new ReadWriteBehavior(ReadEnum, WriteEnum) }, - // Nullable / Optional types - { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) } + // Nullable + { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) }, + + // Option + { type => type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) } }.ToImmutableDictionary(); + private static readonly Dictionary cachedSomeCreateMethods = new Dictionary(); + private static readonly Dictionary cachedNoneCreateMethod = new Dictionary(); + private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => throw new InvalidOperationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement INetSerializableStruct?"); private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new InvalidOperationException($"Type {type} cannot be deserialized. Did you forget to implement INetSerializableStruct?"); + private static void WriteOption(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + Type type = obj.GetType(); + Type optionType = type.GetGenericTypeDefinition(); + Type underlyingType = type.GetGenericArguments()[0]; + + if (optionType == typeof(None<>)) + { + msg.Write(false); + } + else if (optionType == typeof(Some<>)) + { + msg.Write(true); + if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) + { + behavior.WriteAction(obj.Value, attribute, msg); + } + } + else + { + throw new InvalidOperationException("Option type was neither None<> or Some<>"); + } + } + + private static dynamic? ReadOption(IReadMessage inc, Type type, NetworkSerialize attribute) + { + Type underlyingType = type.GetGenericArguments()[0]; + bool hasValue = inc.ReadBoolean(); + if (!hasValue) + { + return GetCreateMethod(typeof(None<>), underlyingType, cachedNoneCreateMethod).Invoke(null, null); + } + + if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) + { + dynamic? value = behavior.ReadAction(inc, underlyingType, attribute); + return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new []{ value }); + } + + throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadOption)}"); + + static MethodInfo GetCreateMethod(Type optionType, Type type, Dictionary cache) + { + if (cache.TryGetValue(type, out MethodInfo? foundInfo)) + { + return foundInfo; + } + + Type genericType = optionType.MakeGenericType(type); + MethodInfo info = genericType.GetMethod("Create", BindingFlags.Static | BindingFlags.Public)!; + cache.Add(type, info); + return info; + } + } + private static void WriteNullable(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) { if (obj is { } notNull) @@ -152,9 +216,9 @@ namespace Barotrauma Range range = GetEnumRange(type); int enumIndex = inc.ReadRangedInteger(range.Start, range.End); - foreach (dynamic e in Enum.GetValues(type)) + foreach (dynamic? e in Enum.GetValues(type)) { - if (Convert.ChangeType(e, e.GetTypeCode()) == enumIndex) { return e; } + if (Convert.ChangeType(e, e!.GetTypeCode()) == enumIndex) { return e; } } throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}"); @@ -213,9 +277,9 @@ namespace Barotrauma msg.WriteRangedInteger(array.Length, 0, attribute.ArrayMaxSize); - foreach (dynamic o in array) + foreach (dynamic? o in array) { - if (TryFindBehavior(o.GetType(), out ReadWriteBehavior behavior)) + if (TryFindBehavior(o!.GetType(), out ReadWriteBehavior behavior)) { behavior.WriteAction(o, attribute, msg); } @@ -285,6 +349,8 @@ namespace Barotrauma private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble(); private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString(); + + private static dynamic ReadIdentifier(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadIdentifier(); private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); @@ -408,8 +474,8 @@ namespace Barotrauma /// string
///
///
- /// In addition arrays, enums and are supported.
- /// Using will make the field or property optional + /// In addition arrays, enums, and are supported.
+ /// Using or will make the field or property optional. /// /// public interface INetSerializableStruct diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index 00dea4a4b..1fed8194f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -13,105 +13,105 @@ namespace Barotrauma public string Name => "KarmaManager"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool ResetKarmaBetweenRounds { get; set; } - [Serialize(0.1f, true)] + [Serialize(0.1f, IsPropertySaveable.Yes)] public float KarmaDecay { get; set; } - [Serialize(50.0f, true)] + [Serialize(50.0f, IsPropertySaveable.Yes)] public float KarmaDecayThreshold { get; set; } - [Serialize(0.15f, true)] + [Serialize(0.15f, IsPropertySaveable.Yes)] public float KarmaIncrease { get; set; } - [Serialize(50.0f, true)] + [Serialize(50.0f, IsPropertySaveable.Yes)] public float KarmaIncreaseThreshold { get; set; } - [Serialize(0.05f, true)] + [Serialize(0.05f, IsPropertySaveable.Yes)] public float StructureRepairKarmaIncrease { get; set; } - [Serialize(0.1f, true)] + [Serialize(0.1f, IsPropertySaveable.Yes)] public float StructureDamageKarmaDecrease { get; set; } - [Serialize(15.0f, true)] + [Serialize(15.0f, IsPropertySaveable.Yes)] public float MaxStructureDamageKarmaDecreasePerSecond { get; set; } - [Serialize(0.03f, true)] + [Serialize(0.03f, IsPropertySaveable.Yes)] public float ItemRepairKarmaIncrease { get; set; } - [Serialize(0.5f, true)] + [Serialize(0.5f, IsPropertySaveable.Yes)] public float ReactorOverheatKarmaDecrease { get; set; } - [Serialize(30.0f, true)] + [Serialize(30.0f, IsPropertySaveable.Yes)] public float ReactorMeltdownKarmaDecrease { get; set; } - [Serialize(0.1f, true)] + [Serialize(0.1f, IsPropertySaveable.Yes)] public float DamageEnemyKarmaIncrease { get; set; } - [Serialize(0.2f, true)] + [Serialize(0.2f, IsPropertySaveable.Yes)] public float HealFriendlyKarmaIncrease { get; set; } - [Serialize(0.25f, true)] + [Serialize(0.25f, IsPropertySaveable.Yes)] public float DamageFriendlyKarmaDecrease { get; set; } - [Serialize(0.25f, true)] + [Serialize(0.25f, IsPropertySaveable.Yes)] public float StunFriendlyKarmaDecrease { get; set; } - [Serialize(0.3f, true)] + [Serialize(0.3f, IsPropertySaveable.Yes)] public float StunFriendlyKarmaDecreaseThreshold { get; set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float ExtinguishFireKarmaIncrease { get; set; } - [Serialize(defaultValue: 15.0f, true)] + [Serialize(defaultValue: 15.0f, IsPropertySaveable.Yes)] public float DangerousItemStealKarmaDecrease { get; set; } - [Serialize(defaultValue: false, true)] + [Serialize(defaultValue: false, IsPropertySaveable.Yes)] public bool DangerousItemStealBots { get; set; } - [Serialize(defaultValue: 0.05f, true)] + [Serialize(defaultValue: 0.05f, IsPropertySaveable.Yes)] public float BallastFloraKarmaIncrease { get; set; } private int allowedWireDisconnectionsPerMinute; - [Serialize(5, true)] + [Serialize(5, IsPropertySaveable.Yes)] public int AllowedWireDisconnectionsPerMinute { get { return allowedWireDisconnectionsPerMinute; } set { allowedWireDisconnectionsPerMinute = Math.Max(0, value); } } - [Serialize(6.0f, true)] + [Serialize(6.0f, IsPropertySaveable.Yes)] public float WireDisconnectionKarmaDecrease { get; set; } - [Serialize(0.15f, true)] + [Serialize(0.15f, IsPropertySaveable.Yes)] public float SteerSubKarmaIncrease { get; set; } - [Serialize(15.0f, true)] + [Serialize(15.0f, IsPropertySaveable.Yes)] public float SpamFilterKarmaDecrease { get; set; } - [Serialize(40.0f, true)] + [Serialize(40.0f, IsPropertySaveable.Yes)] public float HerpesThreshold { get; set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float KickBanThreshold { get; set; } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int KicksBeforeBan { get; set; } - [Serialize(10.0f, true)] + [Serialize(10.0f, IsPropertySaveable.Yes)] public float KarmaNotificationInterval { get; set; } - [Serialize(120.0f, true)] + [Serialize(120.0f, IsPropertySaveable.Yes)] public float AllowedRetaliationTime { get; set; } - [Serialize(5.0f, true)] + [Serialize(5.0f, IsPropertySaveable.Yes)] public float DangerousItemContainKarmaDecrease { get; set; } - [Serialize(defaultValue: true, true)] + [Serialize(defaultValue: true, IsPropertySaveable.Yes)] public bool IsDangerousItemContainKarmaDecreaseIncremental { get; set; } - [Serialize(30.0f, true)] + [Serialize(30.0f, IsPropertySaveable.Yes)] public float MaxDangerousItemContainKarmaDecrease { get; set; } private readonly AfflictionPrefab herpesAffliction; @@ -141,7 +141,7 @@ namespace Barotrauma if (doc?.Root != null) { Presets["custom"] = doc.Root; - foreach (XElement subElement in doc.Root.Elements()) + foreach (var subElement in doc.Root.Elements()) { string presetName = subElement.GetAttributeString("name", ""); Presets[presetName.ToLowerInvariant()] = subElement; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 9314d093d..8580b9ce5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -13,8 +13,6 @@ namespace Barotrauma.Networking public const int ServerNameMaxLength = 60; public const int ServerMessageMaxLength = 2000; - public static string MasterServerUrl = GameMain.Config.MasterServerUrl; - public const float MaxPhysicsBodyVelocity = 64.0f; public const float MaxPhysicsBodyAngularVelocity = 16.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 81170bc0c..af78cac5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -11,17 +11,17 @@ namespace Barotrauma.Networking public readonly Character TargetCharacter; //which entity is this order referring to (hull, reactor, railgun controller, etc) - public readonly ISpatialEntity TargetEntity; + public ISpatialEntity TargetEntity => Order.TargetSpatialEntity; //additional instructions (power up, fire at will, etc) - public readonly string OrderOption; + public Identifier OrderOption => Order.Option; - public readonly int OrderPriority; + public int OrderPriority => Order.ManualPriority; /// /// Used when the order targets a wall /// - public int? WallSectionIndex { get; set; } + public int? WallSectionIndex => Order.WallSectionIndex; public bool IsNewOrder { get; } @@ -29,55 +29,50 @@ namespace Barotrauma.Networking /// Same as calling , /// but the text parameter is set using ///
- public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender, bool isNewOrder = true) - : this(order, orderOption, priority, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, targetCharacter == sender, orderOption, isNewOrder), - targetEntity, targetCharacter, sender, isNewOrder) + public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) + : this(order, + order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + targetCharacter, sender, isNewOrder) { - + } - public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, - Character targetCharacter, Character sender, bool isNewOrder = true) + public OrderChatMessage(Order order, string text, Character targetCharacter, Character sender, bool isNewOrder = true) : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; - OrderOption = orderOption; - OrderPriority = priority; TargetCharacter = targetCharacter; - TargetEntity = targetEntity; IsNewOrder = isNewOrder; } - public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, - string orderOption, int orderPriority, int? wallSectionIndex, bool isNewOrder) + public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, bool isNewOrder) { - msg.Write((byte)Order.PrefabList.IndexOf(order.Prefab)); + msg.Write(order.Prefab.Identifier); msg.Write(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); - msg.Write(targetEntity is Entity ? (targetEntity as Entity).ID : (UInt16)0); + msg.Write(order.TargetSpatialEntity is Entity ? (order.TargetEntity as Entity).ID : (UInt16)0); // The option of a Dismiss order is written differently so we know what order we target // now that the game supports multiple current orders simultaneously - if (order.Prefab.Identifier != "dismissed") + if (!order.IsDismissal) { - msg.Write((byte)Array.IndexOf(order.Prefab.Options, orderOption)); + msg.Write((byte)order.Options.IndexOf(order.Option)); } else { - if (!string.IsNullOrEmpty(orderOption)) + if (order.Option != Identifier.Empty) { msg.Write(true); - string[] dismissedOrder = orderOption.Split('.'); + string[] dismissedOrder = order.Option.Value.Split('.'); msg.Write((byte)dismissedOrder.Length); if (dismissedOrder.Length > 0) { - string dismissedOrderIdentifier = dismissedOrder[0]; - var orderPrefab = Order.GetPrefab(dismissedOrderIdentifier); - msg.Write((byte)Order.PrefabList.IndexOf(orderPrefab)); + Identifier dismissedOrderIdentifier = dismissedOrder[0].ToIdentifier(); + var orderPrefab = OrderPrefab.Prefabs[dismissedOrderIdentifier]; + msg.Write(dismissedOrderIdentifier); if (dismissedOrder.Length > 1) { - string dismissedOrderOption = dismissedOrder[1]; - msg.Write((byte)Array.IndexOf(orderPrefab.Options, dismissedOrderOption)); + Identifier dismissedOrderOption = dismissedOrder[1].ToIdentifier(); + msg.Write((byte)orderPrefab.Options.IndexOf(dismissedOrderOption)); } } } @@ -89,9 +84,9 @@ namespace Barotrauma.Networking } } - msg.Write((byte)orderPriority); + msg.Write((byte)order.ManualPriority); msg.Write((byte)order.TargetType); - if (order.TargetType == Order.OrderTargetType.Position && targetEntity is OrderTarget orderTarget) + if (order.TargetType == Order.OrderTargetType.Position && order.TargetSpatialEntity is OrderTarget orderTarget) { msg.Write(true); msg.Write(orderTarget.Position.X); @@ -103,7 +98,7 @@ namespace Barotrauma.Networking msg.Write(false); if (order.TargetType == Order.OrderTargetType.WallSection) { - msg.Write((byte)(wallSectionIndex ?? order.WallSectionIndex ?? 0)); + msg.Write((byte)(order.WallSectionIndex ?? 0)); } } @@ -112,14 +107,14 @@ namespace Barotrauma.Networking private void WriteOrder(IWriteMessage msg) { - WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex, IsNewOrder); + WriteOrder(msg, Order, TargetCharacter, IsNewOrder); } public struct OrderMessageInfo { - public int OrderIndex { get; } - public Order OrderPrefab { get; } - public string OrderOption { get; } + public Identifier OrderIdentifier { get; } + public OrderPrefab OrderPrefab => OrderPrefab.Prefabs[OrderIdentifier]; + public Identifier OrderOption { get; } public int? OrderOptionIndex { get; } public Character TargetCharacter { get; } public Order.OrderTargetType TargetType { get; } @@ -129,11 +124,10 @@ namespace Barotrauma.Networking public int Priority { get; } public bool IsNewOrder { get; } - public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, + public OrderMessageInfo(Identifier orderIdentifier, Identifier orderOption, int? orderOptionIndex, Character targetCharacter, Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority, bool isNewOrder) { - OrderIndex = orderIndex; - OrderPrefab = orderPrefab; + OrderIdentifier = orderIdentifier; OrderOption = orderOption; OrderOptionIndex = orderOptionIndex; TargetCharacter = targetCharacter; @@ -148,21 +142,20 @@ namespace Barotrauma.Networking public static OrderMessageInfo ReadOrder(IReadMessage msg) { - int orderIndex = msg.ReadByte(); + Identifier orderIdentifier = msg.ReadIdentifier(); ushort targetCharacterId = msg.ReadUInt16(); Character targetCharacter = targetCharacterId != Entity.NullEntityID ? Entity.FindEntityByID(targetCharacterId) as Character : null; ushort targetEntityId = msg.ReadUInt16(); Entity targetEntity = targetEntityId != Entity.NullEntityID ? Entity.FindEntityByID(targetEntityId) : null; - Order orderPrefab = null; int? optionIndex = null; - string orderOption = null; + Identifier orderOption = Identifier.Empty; // The option of a Dismiss order is written differently so we know what order we target // now that the game supports multiple current orders simultaneously - if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + if (orderIdentifier != Identifier.Empty) { - orderPrefab = Order.PrefabList[orderIndex]; - if (orderPrefab.Identifier != "dismissed") + var orderPrefab = OrderPrefab.Prefabs[orderIdentifier]; + if (!orderPrefab.IsDismissal) { optionIndex = msg.ReadByte(); } @@ -172,11 +165,11 @@ namespace Barotrauma.Networking int identifierCount = msg.ReadByte(); if (identifierCount > 0) { - int dismissedOrderIndex = msg.ReadByte(); - Order dismissedOrderPrefab = null; - if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) + Identifier dismissedOrderIdentifier = msg.ReadIdentifier(); + OrderPrefab dismissedOrderPrefab = null; + if (dismissedOrderIdentifier != Identifier.Empty) { - dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; + dismissedOrderPrefab = OrderPrefab.Prefabs[dismissedOrderIdentifier]; orderOption = dismissedOrderPrefab.Identifier; } if (identifierCount > 1) @@ -187,7 +180,7 @@ namespace Barotrauma.Networking var options = dismissedOrderPrefab.Options; if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) { - orderOption += $".{options[dismissedOrderOptionIndex]}"; + orderOption = $"{orderOption.Value}.{options[dismissedOrderOptionIndex]}".ToIdentifier(); } } } @@ -217,9 +210,8 @@ namespace Barotrauma.Networking } bool isNewOrder = msg.ReadBoolean(); - - return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, - orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority, isNewOrder); + return new OrderMessageInfo(orderIdentifier, orderOption, optionIndex, targetCharacter, + orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority, isNewOrder); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs index f7e5e730c..f9e6b81a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs @@ -19,6 +19,7 @@ namespace Barotrauma.Networking Double ReadDouble(); UInt32 ReadVariableUInt32(); String ReadString(); + Identifier ReadIdentifier(); Microsoft.Xna.Framework.Color ReadColorR8G8B8(); Microsoft.Xna.Framework.Color ReadColorR8G8B8A8(); int ReadRangedInteger(int min, int max); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs index 16146f8bf..ae32f3bbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs @@ -19,11 +19,12 @@ namespace Barotrauma.Networking void WriteColorR8G8B8A8(Microsoft.Xna.Framework.Color val); void WriteVariableUInt32(UInt32 val); void Write(string val); + void Write(Identifier val); void WriteRangedInteger(int val, int min, int max); void WriteRangedSingle(Single val, Single min, Single max, int bitCount); void Write(byte[] val, int startIndex, int length); - void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int outLength); + void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int outLength); int BitPosition { get; set; } int BytePosition { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index def9c670e..b39783e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -454,6 +454,7 @@ namespace Barotrauma.Networking { lengthBits = value; seekPos = seekPos > lengthBits ? lengthBits : seekPos; + MsgWriter.EnsureBufferSize(ref buf, lengthBits); } } @@ -540,6 +541,11 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void Write(Identifier val) + { + Write(val.Value); + } + public void WriteRangedInteger(int val, int min, int max) { MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); @@ -555,9 +561,9 @@ namespace Barotrauma.Networking MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); } - public void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int length) + public void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int length) { - if (LengthBytes <= MsgConstants.CompressionThreshold) + if (LengthBytes <= MsgConstants.CompressionThreshold || !compressPastThreshold) { isCompressed = false; if (LengthBytes > outBuf.Length) { Array.Resize(ref outBuf, LengthBytes); } @@ -764,6 +770,11 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Identifier ReadIdentifier() + { + return ReadString().ToIdentifier(); + } + public Color ReadColorR8G8B8() { return MsgReader.ReadColorR8G8B8(buf, ref seekPos); @@ -773,7 +784,6 @@ namespace Barotrauma.Networking { return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); } - public int ReadRangedInteger(int min, int max) { @@ -938,6 +948,10 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void Write(Identifier val) + { + Write(val.Value); + } public void WriteRangedInteger(int val, int min, int max) { @@ -1019,6 +1033,11 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Identifier ReadIdentifier() + { + return ReadString().ToIdentifier(); + } + public Color ReadColorR8G8B8() { return MsgReader.ReadColorR8G8B8(buf, ref seekPos); @@ -1044,7 +1063,7 @@ namespace Barotrauma.Networking return MsgReader.ReadBytes(buf, ref seekPos, numberOfBytes); } - public void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int outLength) + public void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int outLength) { throw new InvalidOperationException("ReadWriteMessages are not to be sent"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index 0c1569709..f78e60edd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -33,7 +33,7 @@ namespace Barotrauma.Networking protected set; } - public string Language + public LanguageIdentifier Language { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 8f73cfc90..50a94dbe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -236,7 +236,7 @@ namespace Barotrauma.Networking //remove respawn items that have been left in the shuttle if (respawnItems.Contains(item)) { - Spawner.AddToRemoveQueue(item); + Spawner.AddItemToRemoveQueue(item); continue; } @@ -272,7 +272,7 @@ namespace Barotrauma.Networking } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != RespawnShuttle) { continue; } hull.OxygenPercentage = 100.0f; @@ -295,12 +295,12 @@ namespace Barotrauma.Networking c.Kill(CauseOfDeathType.Unknown, null, true); c.Enabled = false; - Spawner.AddToRemoveQueue(c); + Spawner.AddEntityToRemoveQueue(c); if (c.Inventory != null) { foreach (Item item in c.Inventory.AllItems) { - Spawner.AddToRemoveQueue(item); + Spawner.AddItemToRemoveQueue(item); } } } @@ -322,7 +322,7 @@ namespace Barotrauma.Networking public static Affliction GetRespawnPenaltyAffliction() { - var respawnPenaltyAffliction = AfflictionPrefab.List.FirstOrDefault(a => a.AfflictionType.Equals("respawnpenalty", StringComparison.OrdinalIgnoreCase)); + var respawnPenaltyAffliction = AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); return respawnPenaltyAffliction?.Instantiate(10.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 8367fdce9..a86284f23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -10,23 +10,20 @@ namespace Barotrauma.Networking { private struct LogMessage { - public readonly string Text; - public readonly string SanitizedText; + public readonly RichString Text; public readonly MessageType Type; - public readonly List RichData; public LogMessage(string text, MessageType type) { if (type.HasFlag(MessageType.Chat)) { - Text = $"[{DateTime.Now}]\n {text}"; + text = $"[{DateTime.Now}]\n {text}"; } else { - Text = $"[{DateTime.Now}]\n {TextManager.GetServerMessage(text)}"; + text = $"[{DateTime.Now}]\n {TextManager.GetServerMessage(text)}"; } - RichData = RichTextData.GetRichTextData(Text, out SanitizedText); - + Text = RichString.Rich(text); Type = type; } } @@ -113,7 +110,7 @@ namespace Barotrauma.Networking var newText = new LogMessage(line, messageType); #if SERVER - DebugConsole.NewMessage(newText.SanitizedText, messageColor[messageType]); //TODO: REMOVE + DebugConsole.NewMessage(newText.Text.SanitizedValue, messageColor[messageType]); //TODO: REMOVE #endif lines.Enqueue(newText); @@ -173,7 +170,7 @@ namespace Barotrauma.Networking try { - File.WriteAllLines(filePath, unsavedLines.Select(l => l.SanitizedText)); + File.WriteAllLines(filePath, unsavedLines.Select(l => l.Text.SanitizedValue)); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index d4031ebc6..00fe33585 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -97,11 +97,8 @@ namespace Barotrauma.Networking private readonly SerializableProperty property; private readonly string typeString; private readonly object parentObject; - - public string Name - { - get { return property.Name; } - } + + public Identifier Name => property.Name.ToIdentifier(); public object Value { @@ -266,7 +263,7 @@ namespace Barotrauma.Networking } }; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -313,7 +310,7 @@ namespace Barotrauma.Networking if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); - UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5); + UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5); if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); @@ -330,7 +327,7 @@ namespace Barotrauma.Networking if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(networkMember.KarmaManager, property, typeName); - UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5); + UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5); if (netProperties.ContainsKey(key)) { throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); } @@ -382,7 +379,7 @@ namespace Barotrauma.Networking public Voting Voting; - public Dictionary MonsterEnabled { get; private set; } + public Dictionary MonsterEnabled { get; private set; } public const int MaxExtraCargoItemsOfType = 10; public const int MaxExtraCargoItemTypes = 20; @@ -405,63 +402,63 @@ namespace Barotrauma.Networking public WhiteList Whitelist { get; private set; } - [Serialize(20, true)] + [Serialize(20, IsPropertySaveable.Yes)] public int TickRate { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool RandomizeSeed { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool UseRespawnShuttle { get; private set; } - [Serialize(300.0f, true)] + [Serialize(300.0f, IsPropertySaveable.Yes)] public float RespawnInterval { get; private set; } - [Serialize(180.0f, true)] + [Serialize(180.0f, IsPropertySaveable.Yes)] public float MaxTransportTime { get; private set; } - [Serialize(0.2f, true)] + [Serialize(0.2f, IsPropertySaveable.Yes)] public float MinRespawnRatio { get; private set; } - [Serialize(60.0f, true)] + [Serialize(60.0f, IsPropertySaveable.Yes)] public float AutoRestartInterval { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool StartWhenClientsReady { get; set; } - [Serialize(0.8f, true)] + [Serialize(0.8f, IsPropertySaveable.Yes)] public float StartWhenClientsReadyRatio { get; @@ -469,7 +466,7 @@ namespace Barotrauma.Networking } private bool allowSpectating; - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowSpectating { get { return allowSpectating; } @@ -481,21 +478,28 @@ namespace Barotrauma.Networking } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool SaveServerLogs { get; private set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] + public bool AllowModDownloads + { + get; + private set; + } = true; + + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowRagdollButton { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowFileTransfers { get; @@ -503,7 +507,7 @@ namespace Barotrauma.Networking } private bool voiceChatEnabled; - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool VoiceChatEnabled { get { return voiceChatEnabled; } @@ -516,7 +520,7 @@ namespace Barotrauma.Networking } private PlayStyle playstyleSelection; - [Serialize(PlayStyle.Casual, true)] + [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get { return playstyleSelection; } @@ -527,14 +531,14 @@ namespace Barotrauma.Networking } } - [Serialize(Barotrauma.LosMode.Opaque, true)] + [Serialize(Barotrauma.LosMode.Opaque, IsPropertySaveable.Yes)] public LosMode LosMode { get; set; } - [Serialize(800, true)] + [Serialize(800, IsPropertySaveable.Yes)] public int LinesPerLogFile { get @@ -569,7 +573,7 @@ namespace Barotrauma.Networking #endif } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowVoteKick { get @@ -582,7 +586,7 @@ namespace Barotrauma.Networking } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowEndVoting { get @@ -596,7 +600,7 @@ namespace Barotrauma.Networking } private bool allowRespawn; - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowRespawn { get { return allowRespawn; ; } @@ -608,28 +612,28 @@ namespace Barotrauma.Networking } } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int BotCount { get; set; } - [Serialize(16, true)] + [Serialize(16, IsPropertySaveable.Yes)] public int MaxBotCount { get; set; } - [Serialize(BotSpawnMode.Normal, true)] + [Serialize(BotSpawnMode.Normal, IsPropertySaveable.Yes)] public BotSpawnMode BotSpawnMode { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DisableBotConversations { get; @@ -642,76 +646,76 @@ namespace Barotrauma.Networking set { selectedLevelDifficulty = MathHelper.Clamp(value, 0.0f, 100.0f); } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowDisguises { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowRewiring { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool LockAllDefaultWires { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool AllowLinkingWifiToChat { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowFriendlyFire { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DestructibleOutposts { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool KillableNPCs { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool BanAfterWrongPassword { get; set; } - [Serialize(3, true)] + [Serialize(3, IsPropertySaveable.Yes)] public int MaxPasswordRetriesBeforeBan { get; private set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string SelectedSubmarine { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string SelectedShuttle { get; @@ -719,7 +723,7 @@ namespace Barotrauma.Networking } private YesNoMaybe traitorsEnabled; - [Serialize(YesNoMaybe.No, true)] + [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] public YesNoMaybe TraitorsEnabled { get { return traitorsEnabled; } @@ -731,35 +735,35 @@ namespace Barotrauma.Networking } } - [Serialize(defaultValue: 1, isSaveable: true)] + [Serialize(defaultValue: 1, isSaveable: IsPropertySaveable.Yes)] public int TraitorsMinPlayerCount { get; set; } - [Serialize(defaultValue: 90.0f, isSaveable: true)] + [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMinStartDelay { get; set; } - [Serialize(defaultValue: 180.0f, isSaveable: true)] + [Serialize(defaultValue: 180.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMaxStartDelay { get; set; } - [Serialize(defaultValue: 30.0f, isSaveable: true)] + [Serialize(defaultValue: 30.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMinRestartDelay { get; set; } - [Serialize(defaultValue: 90.0f, isSaveable: true)] + [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMaxRestartDelay { get; @@ -767,7 +771,7 @@ namespace Barotrauma.Networking } private SelectionMode subSelectionMode; - [Serialize(SelectionMode.Manual, true)] + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] public SelectionMode SubSelectionMode { get { return subSelectionMode; } @@ -780,7 +784,7 @@ namespace Barotrauma.Networking } private SelectionMode modeSelectionMode; - [Serialize(SelectionMode.Manual, true)] + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] public SelectionMode ModeSelectionMode { get { return modeSelectionMode; } @@ -794,42 +798,42 @@ namespace Barotrauma.Networking public BanList BanList { get; private set; } - [Serialize(0.6f, true)] + [Serialize(0.6f, IsPropertySaveable.Yes)] public float EndVoteRequiredRatio { get; private set; } - [Serialize(0.6f, true)] + [Serialize(0.6f, IsPropertySaveable.Yes)] public float SubmarineVoteRequiredRatio { get; private set; } - [Serialize(30f, true)] + [Serialize(30f, IsPropertySaveable.Yes)] public float SubmarineVoteTimeout { get; private set; } - [Serialize(0.6f, true)] + [Serialize(0.6f, IsPropertySaveable.Yes)] public float KickVoteRequiredRatio { get; private set; } - [Serialize(300.0f, true)] + [Serialize(300.0f, IsPropertySaveable.Yes)] public float KillDisconnectedTime { get; private set; } - [Serialize(600.0f, true)] + [Serialize(600.0f, IsPropertySaveable.Yes)] public float KickAFKTime { get; @@ -837,7 +841,7 @@ namespace Barotrauma.Networking } private bool karmaEnabled; - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool KarmaEnabled { get { return karmaEnabled; } @@ -851,7 +855,7 @@ namespace Barotrauma.Networking } private string karmaPreset = "default"; - [Serialize("default", true)] + [Serialize("default", IsPropertySaveable.Yes)] public string KarmaPreset { get { return karmaPreset; } @@ -866,21 +870,21 @@ namespace Barotrauma.Networking } } - [Serialize("sandbox", true)] - public string GameModeIdentifier + [Serialize("sandbox", IsPropertySaveable.Yes)] + public Identifier GameModeIdentifier { get; set; } - [Serialize("All", true)] + [Serialize("All", IsPropertySaveable.Yes)] public string MissionType { get; set; } - [Serialize(8, true)] + [Serialize(8, IsPropertySaveable.Yes)] public int MaxPlayers { get { return maxPlayers; } @@ -893,21 +897,21 @@ namespace Barotrauma.Networking set; } - [Serialize(60f * 60.0f, true)] + [Serialize(60f * 60.0f, IsPropertySaveable.Yes)] public float AutoBanTime { get; private set; } - [Serialize(60.0f * 60.0f * 24.0f, true)] + [Serialize(60.0f * 60.0f * 24.0f, IsPropertySaveable.Yes)] public float MaxAutoBanTime { get; private set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool RadiationEnabled { get; @@ -916,7 +920,7 @@ namespace Barotrauma.Networking private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; - [Serialize(CampaignSettings.DefaultMaxMissionCount, true)] + [Serialize(CampaignSettings.DefaultMaxMissionCount, IsPropertySaveable.Yes)] public int MaxMissionCount { get { return maxMissionCount; } @@ -962,45 +966,40 @@ namespace Barotrauma.Networking /// /// A list of int pairs that represent the ranges of UTF-16 codes allowed in client names /// - public List> AllowedClientNameChars + public List> AllowedClientNameChars { get; private set; - } = new List>(); + } = new List>(); private void InitMonstersEnabled() { //monster spawn settings - if (MonsterEnabled == null) - { - List monsterNames1 = CharacterPrefab.Prefabs.Select(p => p.Identifier).ToList(); - - MonsterEnabled = new Dictionary(); - foreach (string s in monsterNames1) - { - if (!MonsterEnabled.ContainsKey(s)) MonsterEnabled.Add(s, true); - } - } + MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); } public void ReadMonsterEnabled(IReadMessage inc) { InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys.ToList(); - foreach (string s in monsterNames) + List monsterNames = MonsterEnabled.Keys + .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) + .ToList(); + foreach (Identifier s in monsterNames) { MonsterEnabled[s] = inc.ReadBoolean(); } inc.ReadPadBits(); } - public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) + public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) { //monster spawn settings - if (monsterEnabled == null) monsterEnabled = MonsterEnabled; + if (monsterEnabled == null) { monsterEnabled = MonsterEnabled; } - List monsterNames = monsterEnabled.Keys.ToList(); - foreach (string s in monsterNames) + List monsterNames = monsterEnabled.Keys + .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) + .ToList(); + foreach (Identifier s in monsterNames) { msg.Write(monsterEnabled[s]); } @@ -1015,7 +1014,7 @@ namespace Barotrauma.Networking Dictionary extraCargo = new Dictionary(); for (int i = 0; i < count; i++) { - string prefabIdentifier = msg.ReadString(); + Identifier prefabIdentifier = msg.ReadIdentifier(); byte amount = msg.ReadByte(); if (MapEntityPrefab.Find(null, prefabIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab && amount > 0) @@ -1041,7 +1040,7 @@ namespace Barotrauma.Networking msg.Write((UInt32)ExtraCargo.Count); foreach (KeyValuePair kvp in ExtraCargo) { - msg.Write(kvp.Key.Identifier ?? ""); + msg.Write(kvp.Key.Identifier); msg.Write((byte)kvp.Value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 8baf4ad07..ae1f8adf7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -564,7 +564,7 @@ namespace Barotrauma } errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); - if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, GameAnalyticsManager.ErrorSeverity.Error, @@ -591,7 +591,7 @@ namespace Barotrauma } errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); - if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, GameAnalyticsManager.ErrorSeverity.Error, diff --git a/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs b/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs index d47ce7e16..b151d94af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs @@ -17,7 +17,7 @@ namespace Barotrauma #if CLIENT private KeyOrMouse binding { - get { return GameMain.Config.KeyBind(inputType); } + get { return GameSettings.CurrentConfig.KeyMap.Bindings[inputType]; } } private static bool AllowOnGUI(InputType input) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs new file mode 100644 index 000000000..495b5fd86 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + public interface IImplementsVariants where T : Prefab + { + public Identifier VariantOf { get; } + + public void InheritFrom(T parent); + } + + public static class VariantExtensions + { + public static ContentXElement CreateVariantXML(this ContentXElement variantElement, ContentXElement baseElement) + { + #warning TODO: fix %ModDir% instances in the base element such that they become %ModDir:BaseMod% if necessary + return variantElement.Element.CreateVariantXML(baseElement.Element).FromPackage(variantElement.ContentPackage); + } + + public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement) + { + XElement newElement = new XElement(variantElement.Name); + newElement.Add(baseElement.Attributes()); + newElement.Add(baseElement.Elements()); + + ReplaceElement(newElement, variantElement); + + void ReplaceElement(XElement element, XElement replacement) + { + List elementsToRemove = new List(); + foreach (XAttribute attribute in replacement.Attributes()) + { + ReplaceAttribute(element, attribute); + } + foreach (XElement replacementSubElement in replacement.Elements()) + { + int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement); + System.Diagnostics.Debug.Assert(index > -1); + + int i = 0; + bool matchingElementFound = false; + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } + if (i == index) + { + if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements) + { + //if the replacement is empty (no attributes or child elements) + //remove the element from the variant + elementsToRemove.Add(subElement); + } + else + { + ReplaceElement(subElement, replacementSubElement); + } + matchingElementFound = true; + break; + } + i++; + } + if (!matchingElementFound) + { + element.Add(replacementSubElement); + } + } + elementsToRemove.ForEach(e => e.Remove()); + } + + void ReplaceAttribute(XElement element, XAttribute newAttribute) + { + XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (existingAttribute == null) + { + element.Add(newAttribute); + return; + } + float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value); + if (newAttribute.Value.StartsWith('*')) + { + string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier); + if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value * multiplier)).ToString(); + } + } + else if (newAttribute.Value.StartsWith('+')) + { + string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition); + if (additionStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value + addition)).ToString(); + } + } + else + { + existingAttribute.Value = newAttribute.Value; + } + } + + return newElement; + } + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs deleted file mode 100644 index def8398f7..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text; - -namespace Barotrauma -{ - public interface IPrefab - { - string OriginalName { get; } - string Identifier { get; } - string FilePath { get; } - ContentPackage ContentPackage { get; } - } - - public interface IHasUintIdentifier - { - uint UIntIdentifier { get; set; } - } - - public static class PrefabExtensions - { - public static void CalculatePrefabUIntIdentifier(this T prefab, PrefabCollection prefabs) where T : class, IPrefab, IHasUintIdentifier, IDisposable - { - using (MD5 md5 = MD5.Create()) - { - prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); - - //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small - var collision = prefabs.Find(p => p.Identifier != prefab.Identifier && p.UIntIdentifier == prefab.UIntIdentifier); - if (collision != null) - { - DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})"); - collision.UIntIdentifier++; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs new file mode 100644 index 000000000..1b40954db --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs @@ -0,0 +1,63 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Xml.Linq; + +namespace Barotrauma +{ + public abstract class Prefab : IDisposable + { + public readonly static ImmutableHashSet Types; + static Prefab() + { + Types = ReflectionUtils.GetDerivedNonAbstract().ToImmutableHashSet(); + } + + private static bool potentialCallFromConstructor = false; + public static void DisallowCallFromConstructor() + { + if (!potentialCallFromConstructor) { return; } + StackTrace st = new StackTrace(skipFrames: 2, fNeedFileInfo: false); + for (int i = st.FrameCount-1; i >= 0; i--) + { + if (st.GetFrame(i)?.GetMethod() is {IsConstructor: true, DeclaringType: { } declaringType} + && Types.Contains(declaringType)) + { + throw new Exception("Called disallowed method from within a prefab's constructor!"); + } + } + potentialCallFromConstructor = false; + } + + public readonly Identifier Identifier; + public readonly ContentFile ContentFile; + + public ContentPackage? ContentPackage => ContentFile?.ContentPackage; + public ContentPath FilePath => ContentFile.Path; + + public Prefab(ContentFile file, Identifier identifier) + { + potentialCallFromConstructor = true; + ContentFile = file; + Identifier = identifier; + if (Identifier.IsEmpty) { throw new ArgumentException($"Error creating {GetType().Name}: Identifier cannot be empty"); } + } + + public Prefab(ContentFile file, ContentXElement element) + { + potentialCallFromConstructor = true; + ContentFile = file; + Identifier = DetermineIdentifier(element!); + if (Identifier.IsEmpty) { throw new ArgumentException($"Error creating {GetType().Name}: Identifier cannot be empty"); } + } + + protected virtual Identifier DetermineIdentifier(XElement element) + { + return element.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + public abstract void Dispose(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index a238947bb..5c3a16671 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -1,13 +1,70 @@ -using System; +#nullable enable +using Barotrauma.Extensions; +using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Security.Cryptography; namespace Barotrauma { - public class PrefabCollection : IEnumerable where T : class, IPrefab, IDisposable + public class PrefabCollection : IEnumerable where T : notnull, Prefab { + /// + /// Default constructor. + /// + public PrefabCollection() + { + var interfaces = typeof(T).GetInterfaces(); + implementsVariants = interfaces.Any(i => i.Name.Contains(nameof(IImplementsVariants))); + } + + /// + /// Constructor with OnAdd and OnRemove callbacks provided. + /// + public PrefabCollection( + Action? onAdd, + Action? onRemove, + Action? onSort, + Action? onAddOverrideFile, + Action? onRemoveOverrideFile) : this() + { + OnAdd = onAdd; + OnRemove = onRemove; + OnSort = onSort; + OnAddOverrideFile = onAddOverrideFile; + OnRemoveOverrideFile = onRemoveOverrideFile; + } + + /// + /// Method to be called when calling Add(T prefab, bool override). + /// If provided, the method is called only if Add succeeds. + /// + private readonly Action? OnAdd = null; + + /// + /// Method to be called when calling Remove(T prefab). + /// If provided, the method is called before success + /// or failure can be determined within the body of Remove. + /// + private readonly Action? OnRemove = null; + + /// + /// Method to be called when calling SortAll(). + /// + private readonly Action? OnSort = null; + + /// + /// Method to be called when calling AddOverrideFile(ContentFile file). + /// + private readonly Action? OnAddOverrideFile = null; + + /// + /// Method to be called when calling RemoveOverrideFile(ContentFile file). + /// + private readonly Action? OnRemoveOverrideFile = null; + /// /// Dictionary containing all prefabs of the same type. /// Key is the identifier. @@ -18,12 +75,132 @@ namespace Barotrauma /// The last element of the list is the prefab that is effectively used /// (hereby called "active prefab") /// - private readonly Dictionary> prefabs = new Dictionary>(); +#if DEBUG && MODBREAKER + private readonly CursedDictionary> prefabs = new CursedDictionary>(); +#else + private readonly ConcurrentDictionary> prefabs = new ConcurrentDictionary>(); +#endif + + /// + /// Collection of content files that override all previous prefabs + /// i.e. anything set to load before these effectively doesn't exist + /// + private readonly HashSet overrideFiles = new HashSet(); + private ContentFile? topMostOverrideFile = null; + + private readonly bool implementsVariants; + + private bool IsPrefabOverriddenByFile(T prefab) + { + return topMostOverrideFile != null && + topMostOverrideFile.ContentPackage.Index > prefab.ContentFile.ContentPackage.Index; + } + + private class InheritanceTreeCollection + { + public class Node + { + public Node(Identifier identifier) { Identifier = identifier; } + + public readonly Identifier Identifier; + public Node? Parent = null; + public readonly HashSet Inheritors = new HashSet(); + } + + private readonly PrefabCollection prefabCollection; + + public InheritanceTreeCollection(PrefabCollection collection) { prefabCollection = collection; } + + public readonly Dictionary IdToNode = new Dictionary(); + public readonly HashSet RootNodes = new HashSet(); + + public Node? AddNodeAndInheritors(Identifier id) + { + if (!prefabCollection.TryGet(id, out T? prefab)) { return null; } + + if (!IdToNode.TryGetValue(id, out var node)) + { + node = new Node(id); + RootNodes.Add(node); + IdToNode.Add(id, node); + } + else + { + //if the node already exists, it already contains + //all inheritors so let's just return this immediately + return node; + } + + prefabCollection + .Cast>() + .Where(p => p.VariantOf == id) + .Cast() + .ForEach(p => + { + var inheritorNode = AddNodeAndInheritors(p.Identifier); + if (inheritorNode is null) { return; } + RootNodes.Remove(inheritorNode); + inheritorNode.Parent = node; + node.Inheritors.Add(inheritorNode); + }); + + return node; + } + + private void FindCycles(in Node node, HashSet uncheckedNodes) + { + HashSet checkedNodes = new HashSet(); + List hierarchyPositions = new List(); + Node? currNode = node; + do + { + if (!uncheckedNodes.Contains(currNode)) { break; } + if (checkedNodes.Contains(currNode)) + { + int index = hierarchyPositions.IndexOf(currNode); + throw new Exception("Inheritance cycle detected: " + +string.Join(", ", hierarchyPositions.Skip(index).Select(n => n.Identifier))); + } + checkedNodes.Add(currNode); + hierarchyPositions.Add(currNode); + currNode = currNode.Parent; + } while (currNode != null); + uncheckedNodes.RemoveWhere(i => checkedNodes.Contains(i)); + } + + public void AddNodesAndInheritors(IEnumerable ids) + => ids.ForEach(id => AddNodeAndInheritors(id)); + + public void InvokeCallbacks() + { + HashSet uncheckedNodes = IdToNode.Values.ToHashSet(); + IdToNode.Values.ForEach(v => FindCycles(v, uncheckedNodes)); + void invokeCallbacksForNode(Node node) + { + if (!prefabCollection.TryGet(node.Identifier, out var p) || + !(p is IImplementsVariants prefab)) { return; } + if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent)) { prefab.InheritFrom(parent!); } + node.Inheritors.ForEach(invokeCallbacksForNode); + } + RootNodes.ForEach(invokeCallbacksForNode); + } + } + + private void HandleInheritance(Identifier prefabIdentifier) + => HandleInheritance(prefabIdentifier.ToEnumerable()); + + private void HandleInheritance(IEnumerable identifiers) + { + if (!implementsVariants) { return; } + InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this); + inheritanceTreeCollection.AddNodesAndInheritors(identifiers); + inheritanceTreeCollection.InvokeCallbacks(); + } /// /// AllPrefabs exposes all prefabs instead of just the active ones. /// - public IEnumerable>> AllPrefabs + public IEnumerable>> AllPrefabs { get { @@ -35,58 +212,107 @@ namespace Barotrauma } /// - /// Returns the active prefab with the identifier. + /// Returns the active prefab with the given identifier. /// /// Prefab identifier - /// Active prefab with the identifier + /// Active prefab with the given identifier + public T this[Identifier identifier] + { + get + { + Prefab.DisallowCallFromConstructor(); + var prefab = prefabs[identifier].ActivePrefab; + if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + { + return prefab; + } + throw new IndexOutOfRangeException($"Prefab of identifier \"{identifier}\" cannot be returned because it was overridden by \"{topMostOverrideFile!.Path}\""); + } + } + public T this[string identifier] { - get { return prefabs[identifier].Last(); } + get + { + //this exists because I don't want implicit + //string to Identifier conversion for the most + //part, but it's useful and fairly safe to do + //in this particular instance + return this[identifier.ToIdentifier()]; + } } + /// + /// Returns true if a prefab with the identifier exists, false otherwise. + /// + /// Prefab identifier + /// The matching prefab (if one is found) + /// Whether a prefab with the identifier exists or not + public bool TryGet(Identifier identifier, out T? result) + { + Prefab.DisallowCallFromConstructor(); + if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) + { + result = selector!.ActivePrefab; + return true; + } + else + { + result = null; + return false; + } + } + + public bool TryGet(string identifier, out T? result) + => TryGet(identifier.ToIdentifier(), out result); + + public IEnumerable Keys => prefabs.Keys; + /// /// Finds the first active prefab that returns true given the predicate, /// or null if no such prefab is found. /// /// Predicate to perform the search with. /// - public T Find(Predicate predicate) + public T? Find(Predicate predicate) { + Prefab.DisallowCallFromConstructor(); foreach (var kpv in prefabs) { - if (predicate(kpv.Value.Last())) + if (kpv.Value.ActivePrefab is T p && predicate(p)) { - return kpv.Value.Last(); + return p; } } return null; } /// - /// Returns true if a prefab with the identifier exists, false otherwise. + /// Returns true if a prefab with the given identifier exists, false otherwise. /// /// Prefab identifier - /// Whether a prefab with the identifier exists or not - public bool ContainsKey(string identifier) + /// Whether a prefab with the given identifier exists or not + public bool ContainsKey(Identifier identifier) { + Prefab.DisallowCallFromConstructor(); return prefabs.ContainsKey(identifier); } + public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier()); + /// - /// Returns true if a prefab with the identifier exists, false otherwise. + /// Determines whether a prefab is implemented as an override or not. /// - /// Prefab identifier - /// The matching prefab (if one is found) - /// Whether a prefab with the identifier exists or not - public bool TryGetValue(string identifier, out T prefab) + /// Prefab in this collection + /// Whether a prefab is implemented as an override or not + public bool IsOverride(T prefab) { - if (!ContainsKey(identifier)) + Prefab.DisallowCallFromConstructor(); + if (ContainsKey(prefab.Identifier)) { - prefab = default; - return false; + return prefabs[prefab.Identifier].IsOverride(prefab); } - prefab = this[identifier]; - return true; + return false; } /// @@ -100,34 +326,51 @@ namespace Barotrauma /// Is marked as override public void Add(T prefab, bool isOverride) { - if (string.IsNullOrWhiteSpace(prefab.Identifier)) + Prefab.DisallowCallFromConstructor(); + if (prefab.Identifier.IsEmpty) { - DebugConsole.ThrowError($"Prefab \"{prefab.OriginalName}\" has no identifier!"); + throw new ArgumentException($"Prefab has no identifier!"); } - bool basePrefabExists = prefabs.TryGetValue(prefab.Identifier, out List list); - - //Handle bad overrides and duplicates - if (basePrefabExists && !isOverride) - { - DebugConsole.ThrowError($"Failed to add the prefab \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T)}): a prefab with the same identifier already exists; try overriding\n{Environment.StackTrace}"); - return; - } + bool selectorExists = prefabs.TryGetValue(prefab.Identifier, out PrefabSelector? selector); //Add to list - if (!basePrefabExists) + selector ??= new PrefabSelector(); + + if (prefab is PrefabWithUintIdentifier prefabWithUintIdentifier) { - list = new List(); + if (!selector.IsEmpty) + { + prefabWithUintIdentifier.UintIdentifier = (selector.ActivePrefab as PrefabWithUintIdentifier)!.UintIdentifier; + } + else + { + using (MD5 md5 = MD5.Create()) + { + prefabWithUintIdentifier.UintIdentifier = ToolBox.IdentifierToUint32Hash(prefab.Identifier, md5); + + //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small + T? findCollision() + => Find(p => + p.Identifier != prefab.Identifier + && p is PrefabWithUintIdentifier otherPrefab + && otherPrefab.UintIdentifier == prefabWithUintIdentifier.UintIdentifier); + for (T? collision = findCollision(); collision != null; collision = findCollision()) + { + DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})"); + prefabWithUintIdentifier.UintIdentifier++; + } + } + } } + selector.Add(prefab, isOverride); - list.Add(prefab); - - Sort(list); - - if (!basePrefabExists) + if (!selectorExists) { - prefabs.Add(prefab.Identifier, list); + if (!prefabs.TryAdd(prefab.Identifier, selector)) { throw new Exception($"Failed to add selector for \"{prefab.Identifier}\""); } } + OnAdd?.Invoke(prefab, isOverride); + HandleInheritance(prefab.Identifier); } /// @@ -136,62 +379,63 @@ namespace Barotrauma /// Prefab public void Remove(T prefab) { + Prefab.DisallowCallFromConstructor(); + OnRemove?.Invoke(prefab); if (!ContainsKey(prefab.Identifier)) { return; } if (!prefabs[prefab.Identifier].Contains(prefab)) { return; } - if (prefabs[prefab.Identifier].IndexOf(prefab)==0) - { - prefabs[prefab.Identifier][0] = null; - } - else - { - prefabs[prefab.Identifier].Remove(prefab); - } - prefab.Dispose(); + prefabs[prefab.Identifier].Remove(prefab); - if (prefabs[prefab.Identifier].Count <= 0 || - (prefabs[prefab.Identifier].Count == 1 && prefabs[prefab.Identifier][0] == null)) + if (prefabs[prefab.Identifier].IsEmpty) { - prefabs.Remove(prefab.Identifier); + prefabs.TryRemove(prefab.Identifier, out _); } + HandleInheritance(prefab.Identifier); } /// /// Removes all prefabs that were loaded from a certain file. /// - /// File path - public void RemoveByFile(string filePath) + public void RemoveByFile(ContentFile file) { - List prefabsToRemove = new List(); + Prefab.DisallowCallFromConstructor(); + HashSet clearedIdentifiers = new HashSet(); foreach (var kpv in prefabs) { - foreach (var prefab in kpv.Value) - { - if (prefab != null && prefab.FilePath == filePath) - { - prefabsToRemove.Add(prefab); - } - } + kpv.Value.RemoveByFile(file, OnRemove); + if (kpv.Value.IsEmpty) { clearedIdentifiers.Add(kpv.Key); } } - foreach (var prefab in prefabsToRemove) + foreach (var identifier in clearedIdentifiers) { - Remove(prefab); + prefabs.TryRemove(identifier, out _); } + RemoveOverrideFile(file); } /// - /// Sorts a list of prefabs based on the content package load order. + /// Adds an override file to the collection. /// - /// List of prefabs - private void Sort(List list) + public void AddOverrideFile(ContentFile file) { - if (list.Count <= 1) { return; } + Prefab.DisallowCallFromConstructor(); + if (!overrideFiles.Contains(file)) + { + overrideFiles.Add(file); + } + OnAddOverrideFile?.Invoke(file); + } - var newList = list.Skip(1) - .OrderByDescending(p => GameMain.Config.EnabledRegularPackages.IndexOf(p.ContentPackage)).ToList(); - - list.RemoveRange(1, list.Count - 1); - list.AddRange(newList); + /// + /// Removes an override file from the collection. + /// + public void RemoveOverrideFile(ContentFile file) + { + Prefab.DisallowCallFromConstructor(); + if (overrideFiles.Contains(file)) + { + overrideFiles.Remove(file); + } + OnRemoveOverrideFile?.Invoke(file); } /// @@ -199,10 +443,14 @@ namespace Barotrauma /// public void SortAll() { + Prefab.DisallowCallFromConstructor(); foreach (var kvp in prefabs) { - Sort(kvp.Value); + kvp.Value.Sort(); } + topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null; + OnSort?.Invoke(); + HandleInheritance(this.Select(p => p.Identifier)); } /// @@ -211,9 +459,14 @@ namespace Barotrauma /// IEnumerator public IEnumerator GetEnumerator() { + Prefab.DisallowCallFromConstructor(); foreach (var kpv in prefabs) { - yield return kpv.Value.Last(); + var prefab = kpv.Value.ActivePrefab; + if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + { + yield return prefab; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs new file mode 100644 index 000000000..631543c0a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs @@ -0,0 +1,170 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class PrefabSelector : IEnumerable where T : notnull, Prefab + { + public T? BasePrefab + { + get + { + lock (overrides) { return basePrefabInternal; } + } + } + + public T? ActivePrefab + { + get + { + lock (overrides) { return activePrefabInternal; } + } + } + + public void Add(T prefab, bool isOverride) + { + lock (overrides) { AddInternal(prefab, isOverride); } + } + + public void RemoveIfContains(T prefab) + { + lock (overrides) { RemoveIfContainsInternal(prefab); } + } + + public void Remove(T prefab) + { + lock (overrides) { RemoveInternal(prefab); } + } + + public void RemoveByFile(ContentFile file, Action? callback = null) + { + lock (overrides) { RemoveByFileInternal(file, callback); } + } + + public void Sort() + { + lock (overrides) { SortInternal(); } + } + + public bool IsEmpty + { + get + { + lock (overrides) { return isEmptyInternal; } + } + } + + public bool Contains(T prefab) + { + lock (overrides) { return ContainsInternal(prefab); } + } + + public bool IsOverride(T prefab) + { + lock (overrides) { return IsOverrideInternal(prefab); } + } + + + #region Underlying implementations of the public methods, done separately to avoid nested locking + private T? basePrefabInternal; + private readonly List overrides = new List(); + + private T? activePrefabInternal => overrides.Any() ? overrides.First() : basePrefabInternal; + + private void AddInternal(T prefab, bool isOverride) + { + if (isOverride) + { + if (overrides.Contains(prefab)) { throw new InvalidOperationException($"Duplicate prefab in PrefabSelector ({typeof(T)}, {prefab.Identifier}, {prefab.ContentFile.ContentPackage.Name})"); } + overrides.Add(prefab); + } + else + { + if (BasePrefab != null) + { + string prefabName + = prefab is MapEntityPrefab mapEntityPrefab + ? $"\"{mapEntityPrefab.OriginalName}\", \"{prefab.Identifier}\"" + : $"\"{prefab.Identifier}\""; + throw new InvalidOperationException( + $"Failed to add the prefab {prefabName} ({prefab.GetType()}) from \"{prefab.ContentPackage?.Name ?? "[NULL]"}\" ({prefab.ContentPackage?.Dir ?? ""}): " + + $"a prefab with the same identifier from \"{ActivePrefab!.ContentPackage?.Name ?? "[NULL]"}\" ({ActivePrefab!.ContentPackage?.Dir ?? ""}) already exists; try overriding"); + } + basePrefabInternal = prefab; + } + SortInternal(); + } + + private void RemoveIfContainsInternal(T prefab) + { + if (!ContainsInternal(prefab)) { return; } + RemoveInternal(prefab); + } + + private void RemoveInternal(T prefab) + { + if (basePrefabInternal == prefab) { basePrefabInternal = null; } + else if (overrides.Contains(prefab)) { overrides.Remove(prefab); } + else { throw new InvalidOperationException($"Can't remove prefab from PrefabSelector ({typeof(T)}, {prefab.Identifier}, {prefab.ContentFile.ContentPackage.Name})"); } + prefab.Dispose(); + SortInternal(); + } + + private void RemoveByFileInternal(ContentFile file, Action? callback) + { + for (int i = overrides.Count-1; i >= 0; i--) + { + var prefab = overrides[i]; + if (prefab.ContentFile == file) + { + RemoveInternal(prefab); + callback?.Invoke(prefab); + } + } + + if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file) + { + RemoveInternal(basePrefabInternal); + callback?.Invoke(p); + } + } + + private void SortInternal() + { + overrides.Sort((p1, p2) => (p1.ContentPackage?.Index ?? int.MaxValue) - (p2.ContentPackage?.Index ?? int.MaxValue)); + } + + private bool isEmptyInternal => basePrefabInternal is null && !overrides.Any(); + + private bool ContainsInternal(T prefab) => basePrefabInternal == prefab || overrides.Contains(prefab); + + private int IndexOfInternal(T prefab) => basePrefabInternal == prefab + ? overrides.Count + : overrides.IndexOf(prefab); + + private bool IsOverrideInternal(T prefab) => IndexOfInternal(prefab) > 0; + #endregion + + public IEnumerator GetEnumerator() + { + T? basePrefab; + ImmutableArray overrideClone; + lock (overrides) + { + basePrefab = basePrefabInternal; + overrideClone = overrides.ToImmutableArray(); + } + if (basePrefab != null) { yield return basePrefab; } + foreach (T prefab in overrideClone) + { + yield return prefab; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabWithUintIdentifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabWithUintIdentifier.cs new file mode 100644 index 000000000..541f15ed4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabWithUintIdentifier.cs @@ -0,0 +1,20 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + /// + /// Prefab that has a property serves as a deterministic hash of + /// a prefab's identifier. This member is filled automatically + /// by PrefabCollection.Add. Required for GetRandom to work on + /// arbitrary Prefab enumerables, recommended for network synchronization. + /// + public abstract class PrefabWithUintIdentifier : Prefab + { + public UInt32 UintIdentifier { get; set; } + + protected PrefabWithUintIdentifier(ContentFile file, Identifier identifier) : base(file, identifier) { } + + protected PrefabWithUintIdentifier(ContentFile file, ContentXElement element) : base(file, element) { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 93f0a0a79..9b4584325 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -78,7 +78,7 @@ namespace Barotrauma base.Deselect(); #if CLIENT - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); GameMain.SoundManager.SetCategoryMuffle("default", false); GUI.ClearMessages(); #endif @@ -116,37 +116,6 @@ namespace Barotrauma } } } - -#if LINUX - // disgusting - if (PlayerInput.KeyDown(Keys.RightShift) && Character.Controlled is { CharacterHealth: { } health } && PlayerInput.MouseSpeed != Vector2.Zero) - { - AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; - float afflictionAmount = (PlayerInput.MousePosition.X / GameMain.GraphicsWidth) * radiationPrefab.MaxStrength; - Affliction affliction = health.GetAffliction(radiationPrefab.Identifier, true); - - if (affliction == null) - { - health.ApplyAffliction(null, new Affliction(radiationPrefab, Math.Abs(afflictionAmount))); - } - else - { - float diff = affliction.Strength - afflictionAmount; - - if (!MathUtils.NearlyEqual(diff, 0)) - { - if (diff > 0) - { - health.ReduceAffliction(null, radiationPrefab.Identifier, Math.Abs(diff)); - } - else if (diff < 0) - { - health.ApplyAffliction(null, new Affliction(radiationPrefab, Math.Abs(diff))); - } - } - } - } -#endif #endif #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs index 8ddb1ca8c..869d4ccff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs @@ -1,17 +1,12 @@ namespace Barotrauma { - partial class Screen + abstract partial class Screen { - private static Screen selected; - - public static Screen Selected - { - get { return selected; } - } + public static Screen Selected { get; private set; } public static void SelectNull() { - selected = null; + Selected = null; } public virtual void Deselect() @@ -20,9 +15,9 @@ public virtual void Select() { - if (selected != null && selected != this) + if (Selected != null && Selected != this) { - selected.Deselect(); + Selected.Deselect(); #if CLIENT GUIContextMenu.CurrentContextMenu = null; GUI.ClearCursorWait(); @@ -41,14 +36,17 @@ } #endif } - selected = this; + +#if CLIENT + GUI.SettingsMenuOpen = false; +#endif + Selected = this; } - public virtual Camera Cam - { - get { return null; } - } - + public virtual Camera Cam => null; + + public virtual bool IsEditor => false; + public virtual void Update(double deltaTime) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs index eb93b9d5c..c1a63bb65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs @@ -2,14 +2,14 @@ namespace Barotrauma { - interface ISerializableEntity + public interface ISerializableEntity { string Name { get; } - Dictionary SerializableProperties + Dictionary SerializableProperties { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index f5cf90bc5..4809ed0ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -3,6 +3,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using System.Linq; @@ -10,6 +11,8 @@ using System.Reflection; using System.Xml.Linq; using Barotrauma.Networking; +//TODO: come back to this later, clever use of reflection would make this nicer >:) + namespace Barotrauma { [AttributeUsage(AttributeTargets.Property)] @@ -77,7 +80,8 @@ namespace Barotrauma //I would love to see a better way to do this AllowLinkingWifiToChat, IsSwappableItem, - AllowRotating + AllowRotating, + Attachable } public bool IsEditable(ISerializableEntity entity) @@ -94,18 +98,27 @@ namespace Barotrauma { return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen; } + case ConditionType.Attachable: + { + return entity is Holdable holdable && holdable.Attachable; + } } return false; } } + public enum IsPropertySaveable + { + Yes, + No + } [AttributeUsage(AttributeTargets.Property)] public class Serialize : Attribute { - public object defaultValue; - public bool isSaveable; - public string translationTextTag; + public readonly object DefaultValue; + public readonly IsPropertySaveable IsSaveable; + public readonly Identifier TranslationTextTag; /// /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. @@ -122,48 +135,49 @@ namespace Barotrauma /// If set to anything else than null, SerializableEntityEditors will show what the text gets translated to or warn if the text is not found in the language files. /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. /// Setting the value to a non-empty string will let the user select the text from one whose tag starts with the given string (e.g. RoomName. would show all texts with a RoomName.* tag) - public Serialize(object defaultValue, bool isSaveable, string description = "", string translationTextTag = null, bool alwaysUseInstanceValues = false) + public Serialize(object defaultValue, IsPropertySaveable isSaveable, string description = "", string translationTextTag = "", bool alwaysUseInstanceValues = false) { - this.defaultValue = defaultValue; - this.isSaveable = isSaveable; - this.translationTextTag = translationTextTag; + this.DefaultValue = defaultValue; + this.IsSaveable = isSaveable; + this.TranslationTextTag = translationTextTag.ToIdentifier(); Description = description; AlwaysUseInstanceValues = alwaysUseInstanceValues; } } - class SerializableProperty + public class SerializableProperty { - private static Dictionary supportedTypes = new Dictionary + private readonly static ImmutableDictionary supportedTypes = new Dictionary { { typeof(bool), "bool" }, { typeof(int), "int" }, { typeof(float), "float" }, { typeof(string), "string" }, + { typeof(Identifier), "identifier" }, + { typeof(LocalizedString), "localizedstring" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, { typeof(Vector3), "vector3" }, { typeof(Vector4), "vector4" }, { typeof(Rectangle), "rectangle" }, { typeof(Color), "color" }, - { typeof(string[]), "stringarray" } - }; + { typeof(string[]), "stringarray" }, + { typeof(Identifier[]), "identifierarray" } + }.ToImmutableDictionary(); - private static readonly Dictionary> cachedProperties = - new Dictionary>(); + private static readonly Dictionary> cachedProperties = + new Dictionary>(); public readonly string Name; - public readonly string NameToLowerInvariant; public readonly AttributeCollection Attributes; public readonly Type PropertyType; public readonly bool OverridePrefabValues; - public PropertyInfo PropertyInfo { get; private set; } + public readonly PropertyInfo PropertyInfo; public SerializableProperty(PropertyDescriptor property) { Name = property.Name; - NameToLowerInvariant = Name.ToLowerInvariant(); PropertyInfo = property.ComponentType.GetProperty(property.Name); PropertyType = property.PropertyType; Attributes = property.Attributes; @@ -215,8 +229,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value); - DebugConsole.ThrowError("(Type not supported)"); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (Type \"{PropertyType.Name}\" not supported)"); return false; } @@ -274,12 +287,20 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect(value, true)); break; + case "identifier": + PropertyInfo.SetValue(parentObject, value.ToIdentifier()); + break; + case "localizedstring": + PropertyInfo.SetValue(parentObject, new RawLString(value)); + break; case "stringarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value)); break; + case "identifierarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value).ToIdentifiers().ToArray()); + break; } } - catch (Exception e) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); @@ -307,7 +328,8 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value + " (not a valid " + PropertyInfo.PropertyType + ")", e); + DebugConsole.ThrowError( + $"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (not a valid {PropertyInfo.PropertyType})", e); return false; } PropertyInfo.SetValue(parentObject, enumVal); @@ -315,8 +337,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value); - DebugConsole.ThrowError("(Type not supported)"); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (Type \"{PropertyType.Name}\" not supported)"); return false; } @@ -349,9 +370,18 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect((string)value, false)); return true; + case "identifier": + PropertyInfo.SetValue(parentObject, new Identifier((string)value)); + return true; + case "localizedstring": + PropertyInfo.SetValue(parentObject, new RawLString((string)value)); + return true; case "stringarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value)); - break; + return true; + case "identifierarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value).ToIdentifiers().ToArray()); + return true; default: DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")"); @@ -527,6 +557,35 @@ namespace Barotrauma return typeName; } + private readonly ImmutableDictionary> valueGetters = + new Dictionary>() + { + {"Voltage".ToIdentifier(), (obj) => obj is Powered p ? p.Voltage : (object) null}, + {"Charge".ToIdentifier(), (obj) => obj is PowerContainer p ? p.Charge : (object) null}, + {"Overload".ToIdentifier(), (obj) => obj is PowerTransfer p ? p.Overload : (object) null}, + {"AvailableFuel".ToIdentifier(), (obj) => obj is Reactor r ? r.AvailableFuel : (object) null}, + {"FissionRate".ToIdentifier(), (obj) => obj is Reactor r ? r.FissionRate : (object) null}, + {"OxygenFlow".ToIdentifier(), (obj) => obj is Vent v ? v.OxygenFlow : (object) null}, + { + "CurrFlow".ToIdentifier(), + (obj) => obj is Pump p ? (object) p.CurrFlow : + obj is OxygenGenerator o ? (object)o.CurrFlow : + null + }, + {"CurrentVolume".ToIdentifier(), (obj) => obj is Engine e ? e.CurrentVolume : (object)null}, + {"MotionDetected".ToIdentifier(), (obj) => obj is MotionSensor m ? m.MotionDetected : (object)null}, + {"Oxygen".ToIdentifier(), (obj) => obj is Character c ? c.Oxygen : (object)null}, + {"Health".ToIdentifier(), (obj) => obj is Character c ? c.Health : (object)null}, + {"OxygenAvailable".ToIdentifier(), (obj) => obj is Character c ? c.OxygenAvailable : (object)null}, + {"PressureProtection".ToIdentifier(), (obj) => obj is Character c ? c.PressureProtection : (object)null}, + {"IsDead".ToIdentifier(), (obj) => obj is Character c ? c.IsDead : (object)null}, + {"IsHuman".ToIdentifier(), (obj) => obj is Character c ? c.IsHuman : (object)null}, + {"IsOn".ToIdentifier(), (obj) => obj is LightComponent l ? l.IsOn : (object)null}, + {"Condition".ToIdentifier(), (obj) => obj is Item i ? i.Condition : (object)null}, + {"ContainerIdentifier".ToIdentifier(), (obj) => obj is Item i ? i.ContainerIdentifier : (object)null}, + {"PhysicsBodyActive".ToIdentifier(), (obj) => obj is Item i ? i.PhysicsBodyActive : (object)null}, + }.ToImmutableDictionary(); + /// /// Try getting the values of some commonly used properties directly without reflection /// @@ -683,7 +742,7 @@ namespace Barotrauma { case nameof(Item.ContainerIdentifier): { - if (parentObject is Item item) { value = item.ContainerIdentifier; return true; } + if (parentObject is Item item) { value = item.ContainerIdentifier.Value; return true; } } break; } @@ -761,7 +820,7 @@ namespace Barotrauma return editableProperties; } - public static Dictionary GetProperties(object obj) + public static Dictionary GetProperties(object obj) { Type objType = obj.GetType(); if (cachedProperties.ContainsKey(objType)) @@ -770,11 +829,11 @@ namespace Barotrauma } var properties = TypeDescriptor.GetProperties(obj.GetType()).Cast(); - Dictionary dictionary = new Dictionary(); + Dictionary dictionary = new Dictionary(); foreach (var property in properties) { var serializableProperty = new SerializableProperty(property); - dictionary.Add(serializableProperty.NameToLowerInvariant, serializableProperty); + dictionary.Add(serializableProperty.Name.ToIdentifier(), serializableProperty); } cachedProperties[objType] = dictionary; @@ -782,16 +841,16 @@ namespace Barotrauma return dictionary; } - public static Dictionary DeserializeProperties(object obj, XElement element = null) + public static Dictionary DeserializeProperties(object obj, XElement element = null) { - Dictionary dictionary = GetProperties(obj); + Dictionary dictionary = GetProperties(obj); foreach (var property in dictionary.Values) { //set the value of the property to the default value if there is one foreach (var ini in property.Attributes.OfType()) { - property.TrySetValue(obj, ini.defaultValue); + property.TrySetValue(obj, ini.DefaultValue); break; } } @@ -802,7 +861,7 @@ namespace Barotrauma //and set the value of the matching property if it is initializable foreach (XAttribute attribute in element.Attributes()) { - if (!dictionary.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } + if (!dictionary.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; } if (!property.Attributes.OfType().Any()) { continue; } property.TrySetValue(obj, attribute.Value); } @@ -827,7 +886,7 @@ namespace Barotrauma bool save = false; foreach (var attribute in property.Attributes.OfType()) { - if ((attribute.isSaveable && !attribute.defaultValue.Equals(value)) || + if ((attribute.IsSaveable == IsPropertySaveable.Yes && !attribute.DefaultValue.Equals(value)) || (!ignoreEditable && property.Attributes.OfType().Any())) { save = true; @@ -881,6 +940,10 @@ namespace Barotrauma string[] stringArray = (string[])value; stringValue = stringArray != null ? string.Join(';', stringArray) : ""; break; + case "identifierarray": + Identifier[] identifierArray = (Identifier[])value; + stringValue = identifierArray != null ? string.Join(';', identifierArray) : ""; + break; default: stringValue = value.ToString(); break; @@ -888,7 +951,7 @@ namespace Barotrauma } element.Attribute(property.Name)?.Remove(); - element.SetAttributeValue(property.NameToLowerInvariant, stringValue); + element.SetAttributeValue(property.Name, stringValue); } } @@ -899,9 +962,9 @@ namespace Barotrauma /// The entity to upgrade /// The XML element to get the upgrade instructions from (e.g. the config of an item prefab) /// The game version the entity was saved with - public static void UpgradeGameVersion(ISerializableEntity entity, XElement configElement, Version savedVersion) + public static void UpgradeGameVersion(ISerializableEntity entity, ContentXElement configElement, Version savedVersion) { - foreach (XElement subElement in configElement.Elements()) + foreach (var subElement in configElement.Elements()) { if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); @@ -909,7 +972,7 @@ namespace Barotrauma foreach (XAttribute attribute in subElement.Attributes()) { - string attributeName = attribute.Name.ToString().ToLowerInvariant(); + var attributeName = attribute.NameAsIdentifier(); if (attributeName == "gameversion") { continue; } if (attributeName == "refreshrect") @@ -1024,19 +1087,19 @@ namespace Barotrauma if (entity is Item item2) { - XElement componentElement = subElement.FirstElement(); + var componentElement = subElement.FirstElement(); if (componentElement == null) { continue; } ItemComponent itemComponent = item2.Components.First(c => c.Name == componentElement.Name.ToString()); if (itemComponent == null) { continue; } foreach (XAttribute attribute in componentElement.Attributes()) { - string attributeName = attribute.Name.ToString().ToLowerInvariant(); + var attributeName = attribute.NameAsIdentifier(); if (itemComponent.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { FixValue(property, itemComponent, attribute); } } - foreach (XElement element in componentElement.Elements()) + foreach (var element in componentElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs new file mode 100644 index 000000000..409e5d5b1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs @@ -0,0 +1,233 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using System.Xml.Schema; + +namespace Barotrauma +{ + public static class StructSerialization + { + private static readonly ImmutableDictionary deserializeMethods; + private static readonly ImmutableDictionary serializeMethods; + + public class SkipAttribute : Attribute { } + + private static bool ShouldSkip(this FieldInfo field) + => field.GetCustomAttribute() != null; + + private static HandlerAttribute? ExtractHandler(this FieldInfo field) + => field.GetCustomAttribute(); + + public class HandlerAttribute : Attribute + { + public readonly Func Read; + public readonly Func Write; + + public HandlerAttribute(Type handlerType) + { + var readAction = + handlerType.GetMethod(nameof(Read), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Type {handlerType.Name} does not have a static {nameof(Read)} method"); + var writeAction = + handlerType.GetMethod(nameof(Write), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Type {handlerType.Name} does not have a static {nameof(Write)} method"); + var paramArray = new object?[1]; + Read = (s) => + { + paramArray[0] = s; + return readAction.Invoke(null, paramArray); + }; + Write = (o) => + { + paramArray[0] = o; + return writeAction.Invoke(null, paramArray)?.ToString(); + }; + } + } + + static StructSerialization() + { + deserializeMethods = + typeof(StructSerialization) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => + { + if (!m.Name.StartsWith("Deserialize")) { return false; } + var parameters = m.GetParameters(); + if (parameters.Length < 1 || parameters.Length > 2 || + parameters[0].ParameterType != typeof(string)) + { + return false; + } + return true; + }) + .Select(m => (m.ReturnType, m)) + .ToImmutableDictionary(); + + serializeMethods = + typeof(StructSerialization) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => + { + if (!m.Name.StartsWith("Serialize")) { return false; } + var parameters = m.GetParameters(); + if (parameters.Length != 1 || + m.ReturnType != typeof(string)) + { + return false; + } + return true; + }) + .Select(m => (m.GetParameters()[0].ParameterType, m)) + .ToImmutableDictionary(); + } + + public static void CopyPropertiesFrom(this ref T self, in T other) where T : struct + { + var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).Where(f => !f.IsInitOnly).ToArray(); + foreach (var field in fields) + { + if (field.ShouldSkip()) { continue; } + field.SetValue(self, field.GetValue(other)); + } + } + + public static void DeserializeElement(this ref T self, XElement element) where T : struct + { + var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).ToArray(); + + //box the struct here so we don't end up + //making a copy every time we feed this to reflection + object boxedSelf = self; + foreach (var field in fields) + { + if (field.ShouldSkip()) { continue; } + boxedSelf.TryDeserialize(field, element); + } + //copy the boxed struct into the original + self = (T)boxedSelf; + } + + public static void TryDeserialize(this object boxedSelf, FieldInfo field, XElement element) + { + string fieldName = field.Name.ToLowerInvariant(); + string valueStr = element.GetAttributeString(fieldName, field.GetValue(boxedSelf)?.ToString() ?? ""); + + var handler = field.ExtractHandler(); + + if (handler != null) + { + field.SetValue(boxedSelf, handler.Read(valueStr)); + } + else if (deserializeMethods.TryGetValue(field.FieldType, out MethodInfo? deserializeMethod)) + { + object?[] parameters = { valueStr }; + if (deserializeMethod.GetParameters().Length > 1) + { + Array.Resize(ref parameters, 2); + parameters[1] = field.GetValue(boxedSelf); + } + field.SetValue(boxedSelf, deserializeMethod.Invoke(boxedSelf, parameters)); + } + else if (field.FieldType.IsEnum) + { + field.SetValue(boxedSelf, DeserializeEnum(field.FieldType, valueStr, (Enum)field.GetValue(boxedSelf)!)); + } + } + + public static string DeserializeString(string str) + { + return str; + } + + public static bool DeserializeBool(string str, bool defaultValue) + { + if (bool.TryParse(str, out bool result)) { return result; } + return defaultValue; + } + + public static float DeserializeFloat(string str, float defaultValue) + { + if (float.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out float result)) { return result; } + return defaultValue; + } + + public static Int32 DeserializeInt32(string str, Int32 defaultValue) + { + if (Int32.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out Int32 result)) { return result; } + return defaultValue; + } + + public static Identifier DeserializeIdentifier(string str) + { + return str.ToIdentifier(); + } + + public static LanguageIdentifier DeserializeLanguageIdentifier(string str) + { + return str.ToLanguageIdentifier(); + } + + public static Color DeserializeColor(string str) + { + return XMLExtensions.ParseColor(str); + } + + public static Enum DeserializeEnum(Type enumType, string str, Enum defaultValue) + { + if (Enum.TryParse(enumType, str, out object? result)) { return (Enum)result!; } + return defaultValue; + } + + public static void SerializeElement(this ref T self, XElement element) where T : struct + { + var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).ToArray(); + + foreach (var field in fields) + { + if (field.ShouldSkip()) { continue; } + self.TrySerialize(field, element); + } + } + + public static void TrySerialize(this T self, FieldInfo field, XElement element) where T : struct + { + string fieldName = field.Name.ToLowerInvariant(); + object? fieldValue = field.GetValue(self); + + string valueStr = fieldValue?.ToString() ?? ""; + + var handler = field.ExtractHandler(); + + if (handler != null) + { + valueStr = handler.Write(valueStr) ?? ""; + } + else if (serializeMethods.TryGetValue(field.FieldType, out MethodInfo? method)) + { + object?[] parameters = { fieldValue }; + valueStr = (string)method.Invoke(self, parameters)!; + } + + element.SetAttributeValue(fieldName, valueStr); + } + + public static string SerializeBool(bool val) + => val ? "true" : "false"; + + public static string SerializeInt32(Int32 val) + => val.ToString(CultureInfo.InvariantCulture); + + public static string SerializeFloat(float val) + => val.ToString(CultureInfo.InvariantCulture); + + public static string SerializeColor(Color val) + => val.ToStringHex(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 91e9b82a5..43b51b551 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -69,8 +70,25 @@ namespace Barotrauma return doc; } + public static XDocument TryLoadXml(ContentPath path) => TryLoadXml(path.Value); + public static XDocument TryLoadXml(string filePath) { + var doc = TryLoadXml(filePath, out var exception); + if (exception != null) + { + DebugConsole.ThrowError($"Couldn't load xml document \"{filePath}\"!", exception); + } + else if (doc is null) + { + DebugConsole.ThrowError($"File \"{filePath}\" could not be loaded: Document or the root element is invalid!"); + } + return doc; + } + + public static XDocument TryLoadXml(string filePath, out Exception exception) + { + exception = null; XDocument doc; try { @@ -81,42 +99,16 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Couldn't load xml document \"" + filePath + "\"!", e); + exception = e; return null; } if (doc?.Root == null) { - DebugConsole.ThrowError("File \"" + filePath + "\" could not be loaded: Document or the root element is invalid!"); return null; } return doc; } - public static XDocument LoadXml(string filePath) - { - XDocument doc = null; - - ToolBox.IsProperFilenameCase(filePath); - - if (File.Exists(filePath)) - { - try - { - using FileStream stream = File.Open(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read); - using XmlReader reader = CreateReader(stream, Path.GetFullPath(filePath)); - doc = XDocument.Load(reader); - } - catch - { - return null; - } - - if (doc.Root == null) { return null; } - } - - return doc; - } - public static object GetAttributeObject(XAttribute attribute) { if (attribute == null) { return null; } @@ -151,8 +143,48 @@ namespace Barotrauma public static string GetAttributeString(this XElement element, string name, string defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return GetAttributeString(element.Attribute(name), defaultValue); + if (element?.GetAttribute(name) == null) { return defaultValue; } + string str = GetAttributeString(element.GetAttribute(name), defaultValue); +#if DEBUG + if (!str.IsNullOrEmpty() && + (str.Contains("%ModDir", StringComparison.OrdinalIgnoreCase) + || str.CleanUpPathCrossPlatform(correctFilenameCase: false).StartsWith("Content/", StringComparison.OrdinalIgnoreCase))) + { + DebugConsole.ThrowError($"Use {nameof(GetAttributeContentPath)} instead of {nameof(GetAttributeString)}\n{Environment.StackTrace.CleanupStackTrace()}"); + if (Debugger.IsAttached) { Debugger.Break(); } + } +#endif + return str; + } + + public static string GetAttributeStringUnrestricted(this XElement element, string name, string defaultValue) + { + #warning TODO: remove? + if (element?.GetAttribute(name) == null) { return defaultValue; } + return GetAttributeString(element.GetAttribute(name), defaultValue); + } + + public static bool DoesAttributeReferenceFileNameAlone(this XElement element, string name) + { + string texName = element.GetAttributeStringUnrestricted(name, ""); + return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase); + } + + public static ContentPath GetAttributeContentPath(this XElement element, string name, + ContentPackage contentPackage) + { + if (element?.GetAttribute(name) == null) { return null; } + return ContentPath.FromRaw(contentPackage, GetAttributeString(element.GetAttribute(name), null)); + } + + public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue) + { + return element.GetAttributeString(name, defaultValue).ToIdentifier(); + } + + public static Identifier GetAttributeIdentifier(this XElement element, string name, Identifier defaultValue) + { + return element.GetAttributeIdentifier(name, defaultValue.Value); } private static string GetAttributeString(XAttribute attribute, string defaultValue) @@ -163,43 +195,41 @@ namespace Barotrauma public static string[] GetAttributeStringArray(this XElement element, string name, string[] defaultValue, bool trim = true, bool convertToLowerInvariant = false) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(',', ','); - if (convertToLowerInvariant) + for (int i = 0; i < splitValue.Length; i++) { - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].ToLowerInvariant(); - } - } - if (trim) - { - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].Trim(); - } + if (convertToLowerInvariant) { splitValue[i] = splitValue[i].ToLowerInvariant(); } + if (trim) { splitValue[i] = splitValue[i].Trim(); } } return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) + { + return element.GetAttributeStringArray(name, null, trim: trim, convertToLowerInvariant: false) + ?.ToIdentifiers() + ?? defaultValue; + } + public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName) { if (element == null) { return defaultValue; } foreach (string name in matchingAttributeName) { - if (element.Attribute(name) == null) { continue; } + if (element.GetAttribute(name) == null) { continue; } float val; try { - string strVal = element.Attribute(name).Value; + string strVal = element.GetAttribute(name).Value; if (strVal.LastOrDefault() == 'f') { strVal = strVal.Substring(0, strVal.Length - 1); @@ -219,12 +249,12 @@ namespace Barotrauma public static float GetAttributeFloat(this XElement element, string name, float defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } float val = defaultValue; try { - string strVal = element.Attribute(name).Value; + string strVal = element.GetAttribute(name).Value; if (strVal.LastOrDefault() == 'f') { strVal = strVal.Substring(0, strVal.Length - 1); @@ -286,9 +316,9 @@ namespace Barotrauma public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -315,15 +345,15 @@ namespace Barotrauma public static int GetAttributeInt(this XElement element, string name, int defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } int val = defaultValue; try { - if (!Int32.TryParse(element.Attribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) + if (!Int32.TryParse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) { - val = (int)float.Parse(element.Attribute(name).Value, CultureInfo.InvariantCulture); + val = (int)float.Parse(element.GetAttribute(name).Value, CultureInfo.InvariantCulture); } } catch (Exception e) @@ -336,13 +366,13 @@ namespace Barotrauma public static uint GetAttributeUInt(this XElement element, string name, uint defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } uint val = defaultValue; try { - val = UInt32.Parse(element.Attribute(name).Value); + val = UInt32.Parse(element.GetAttribute(name).Value); } catch (Exception e) { @@ -354,13 +384,31 @@ namespace Barotrauma public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = UInt64.Parse(element.Attribute(name).Value); + val = UInt64.Parse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "! ", e); + } + + return val; + } + + public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue) + { + if (element?.GetAttribute(name) == null) return defaultValue; + + Version val = defaultValue; + + try + { + val = Version.Parse(element.GetAttribute(name).Value); } catch (Exception e) { @@ -372,13 +420,13 @@ namespace Barotrauma public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = Steam.SteamManager.SteamIDStringToUInt64(element.Attribute(name).Value); + val = Steam.SteamManager.SteamIDStringToUInt64(element.GetAttribute(name).Value); } catch (Exception e) { @@ -390,9 +438,9 @@ namespace Barotrauma public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -414,9 +462,9 @@ namespace Barotrauma } public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -448,8 +496,8 @@ namespace Barotrauma public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return element.Attribute(name).GetAttributeBool(defaultValue); + if (element?.GetAttribute(name) == null) { return defaultValue; } + return element.GetAttribute(name).GetAttributeBool(defaultValue); } public static bool GetAttributeBool(this XAttribute attribute, bool defaultValue) @@ -472,45 +520,45 @@ namespace Barotrauma public static Point GetAttributePoint(this XElement element, string name, Point defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return ParsePoint(element.Attribute(name).Value); + if (element?.GetAttribute(name) == null) { return defaultValue; } + return ParsePoint(element.GetAttribute(name).Value); } public static Vector2 GetAttributeVector2(this XElement element, string name, Vector2 defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return ParseVector2(element.Attribute(name).Value); + if (element?.GetAttribute(name) == null) { return defaultValue; } + return ParseVector2(element.GetAttribute(name).Value); } public static Vector3 GetAttributeVector3(this XElement element, string name, Vector3 defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseVector3(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseVector3(element.GetAttribute(name).Value); } public static Vector4 GetAttributeVector4(this XElement element, string name, Vector4 defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseVector4(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseVector4(element.GetAttribute(name).Value); } public static Color GetAttributeColor(this XElement element, string name, Color defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseColor(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseColor(element.GetAttribute(name).Value); } public static Color? GetAttributeColor(this XElement element, string name) { - if (element == null || element.Attribute(name) == null) { return null; } - return ParseColor(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return null; } + return ParseColor(element.GetAttribute(name).Value); } public static Color[] GetAttributeColorArray(this XElement element, string name, Color[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(';'); @@ -531,10 +579,31 @@ namespace Barotrauma return colorValue; } +#if CLIENT + public static KeyOrMouse GetAttributeKeyOrMouse(this XElement element, string name, KeyOrMouse defaultValue) + { + string strValue = element.GetAttributeString(name, defaultValue?.ToString() ?? ""); + if (Enum.TryParse(strValue, true, out Microsoft.Xna.Framework.Input.Keys key)) + { + return key; + } + else if (Enum.TryParse(strValue, out MouseButton mouseButton)) + { + return mouseButton; + } + else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) && + (Enum.GetValues(typeof(MouseButton)) as MouseButton[]).Contains((MouseButton)mouseButtonInt)) + { + return (MouseButton)mouseButtonInt; + } + return defaultValue; + } +#endif + public static Rectangle GetAttributeRect(this XElement element, string name, Rectangle defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseRect(element.Attribute(name).Value, false); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseRect(element.GetAttribute(name).Value, false); } //TODO: nested tuples and and n-uples where n!=2 are unsupported @@ -703,16 +772,10 @@ namespace Barotrauma if (stringColor.StartsWith("gui.", StringComparison.OrdinalIgnoreCase)) { #if CLIENT - if (GUI.Style != null) + Identifier colorName = stringColor.Substring(4).ToIdentifier(); + if (GUIStyle.Colors.TryGetValue(colorName, out GUIColor guiColor)) { - string colorName = stringColor.Substring(4); - var property = GUI.Style.GetType().GetProperties().FirstOrDefault( - p => p.PropertyType == typeof(Color) && - p.Name.Equals(colorName, StringComparison.OrdinalIgnoreCase)); - if (property != null) - { - return (Color)property?.GetValue(GUI.Style); - } + return guiColor.Value; } #endif return Color.White; @@ -727,7 +790,7 @@ namespace Barotrauma if (strComponents.Length == 1) { - bool hexFailed = true; + bool altParseFailed = true; stringColor = stringColor.Trim(); if (stringColor.Length > 0 && stringColor[0] == '#') { @@ -744,11 +807,40 @@ namespace Barotrauma components[2] = ((float)((colorInt & 0x0000ff00) >> 8)) / 255.0f; components[3] = ((float)(colorInt & 0x000000ff)) / 255.0f; - hexFailed = false; + altParseFailed = false; + } + } + else if (stringColor.Length > 0 && stringColor[0] == '{') + { + stringColor = stringColor.Substring(1, stringColor.Length-2); + + string[] mgComponents = stringColor.Split(' '); + if (mgComponents.Length == 4) + { + altParseFailed = false; + + string[] expectedPrefixes = {"R:", "G:", "B:", "A:"}; + for (int i = 0; i < 4; i++) + { + if (mgComponents[i].StartsWith(expectedPrefixes[i], StringComparison.OrdinalIgnoreCase)) + { + string strToParse = mgComponents[i] + .Remove(expectedPrefixes[i], StringComparison.OrdinalIgnoreCase) + .Trim(); + int val = 0; + altParseFailed |= !int.TryParse(strToParse, out val); + components[i] = ((float) val) / 255f; + } + else + { + altParseFailed = true; + break; + } + } } } - if (hexFailed) + if (altParseFailed) { if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); } return Color.White; @@ -807,18 +899,27 @@ namespace Barotrauma return floatArray; } + public static Identifier VariantOf(this XElement element) => + element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", "")); + public static string[] ParseStringArray(string stringArrayValues) { - return string.IsNullOrEmpty(stringArrayValues) ? new string[0] : stringArrayValues.Split(';'); + return string.IsNullOrEmpty(stringArrayValues) ? Array.Empty() : stringArrayValues.Split(';'); + } + + public static Identifier[] ParseIdentifierArray(string stringArrayValues) + { + return ParseStringArray(stringArrayValues).ToIdentifiers().ToArray(); } - public static bool IsOverride(this XElement element) => element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase); - public static bool IsCharacterVariant(this XElement element) => element.Name.ToString().Equals("charactervariant", StringComparison.OrdinalIgnoreCase); + public static bool IsOverride(this XElement element) => element.NameAsIdentifier() == "override"; public static XElement FirstElement(this XElement element) => element.Elements().FirstOrDefault(); public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod)); + public static XAttribute GetAttribute(this XElement element, Identifier name) => element.GetAttribute(name.Value, StringComparison.OrdinalIgnoreCase); + public static XAttribute GetAttribute(this XElement element, Func predicate) => element.Attributes().FirstOrDefault(predicate); /// @@ -830,5 +931,31 @@ namespace Barotrauma /// Returns all child elements that match the name using the provided comparison method. /// public static IEnumerable GetChildElements(this XContainer container, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => container.Elements().Where(e => e.Name.ToString().Equals(name, comparisonMethod)); + + public static IEnumerable GetChildElements(this XContainer container, params string[] names) + { + return names.SelectMany(name => container.GetChildElements(name)); + } + + public static bool ComesAfter(this XElement element, XElement other) + { + if (element.Parent != other.Parent) { return false; } + foreach (var child in element.Parent.Elements()) + { + if (child == element) { return false; } + if (child == other) { return true; } + } + return false; + } + + public static Identifier NameAsIdentifier(this XElement elem) + { + return elem.Name.LocalName.ToIdentifier(); + } + + public static Identifier NameAsIdentifier(this XAttribute attr) + { + return attr.Name.LocalName.ToIdentifier(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs new file mode 100644 index 000000000..5886c8ba3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Barotrauma +{ + public class CreatureMetrics + { + public readonly HashSet RecentlyEncountered = new HashSet(); + public readonly HashSet Encountered = new HashSet(); + public readonly HashSet Killed = new HashSet(); + + public readonly static CreatureMetrics Instance = new CreatureMetrics(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs new file mode 100644 index 000000000..b917b844a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -0,0 +1,562 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; +#if CLIENT +using Barotrauma.ClientSource.Settings; +using Microsoft.Xna.Framework.Input; +#endif + +namespace Barotrauma +{ + public enum WindowMode + { + Windowed, Fullscreen, BorderlessWindowed + } + + public enum LosMode + { + None = 0, + Transparent = 1, + Opaque = 2 + } + + public enum VoiceMode + { + Disabled, + PushToTalk, + Activity + } + + public static class GameSettings + { + public struct Config + { + public static Config GetDefault() + { + Config config = new Config + { + Language = LanguageIdentifier.None, + SubEditorUndoBuffer = 32, + MaxAutoSaves = 8, + AutoSaveIntervalSeconds = 300, + SubEditorBackground = new Color(13, 37, 69, 255), + EnableSplashScreen = true, + PauseOnFocusLost = true, + AimAssistAmount = 0.5f, + EnableMouseLook = true, + ChatOpen = true, + CrewMenuOpen = true, + CampaignDisclaimerShown = false, + EditorDisclaimerShown = false, + ShowOffensiveServerPrompt = true, + TutorialSkipWarning = true, + CorpseDespawnDelay = 600, + CorpsesPerSubDespawnThreshold = 5, + #if OSX + UseDualModeSockets = false, + #else + UseDualModeSockets = true, + #endif + DisableInGameHints = false, + EnableSubmarineAutoSave = true, + Graphics = GraphicsSettings.GetDefault(), + Audio = AudioSettings.GetDefault(), +#if CLIENT + KeyMap = KeyMapping.GetDefault(), + InventoryKeyMap = InventoryKeyMapping.GetDefault() +#endif + + }; +#if DEBUG + config.UseSteamMatchmaking = true; + config.QuickStartSub = "Humpback".ToIdentifier(); + config.RequireSteamAuthentication = true; + config.AutomaticQuickStartEnabled = false; + config.AutomaticCampaignLoadEnabled = false; + config.TextManagerDebugModeEnabled = false; + config.ModBreakerMode = false; +#endif + return config; + } + + public static Config FromFile(string configFile, in Config? fallback = null) + { + XDocument doc = XMLExtensions.TryLoadXml(configFile); + + return FromElement(doc.Root ?? throw new InvalidOperationException("Unable to load config file: XML document is null."), fallback); + } + + public static Config FromElement(XElement element, in Config? fallback = null) + { + Config retVal = fallback ?? GetDefault(); + + retVal.DeserializeElement(element); + + retVal.Graphics = GraphicsSettings.FromElements(element.GetChildElements("graphicsmode", "graphicssettings"), retVal.Graphics); + retVal.Audio = AudioSettings.FromElements(element.GetChildElements("audio"), retVal.Audio); +#if CLIENT + retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap); + retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap); +#endif + + return retVal; + } + + public LanguageIdentifier Language; + public bool VerboseLogging; + public bool SaveDebugConsoleLogs; + public int SubEditorUndoBuffer; + public int MaxAutoSaves; + public int AutoSaveIntervalSeconds; + public Color SubEditorBackground; + public bool EnableSplashScreen; + public bool PauseOnFocusLost; + public float AimAssistAmount; + public bool EnableMouseLook; + public bool ChatOpen; + public bool CrewMenuOpen; + public bool CampaignDisclaimerShown; + public bool EditorDisclaimerShown; + public bool ShowOffensiveServerPrompt; + public bool TutorialSkipWarning; + public int CorpseDespawnDelay; + public int CorpsesPerSubDespawnThreshold; + public bool UseDualModeSockets; + public bool DisableInGameHints; + public bool EnableSubmarineAutoSave; + public Identifier QuickStartSub; +#if DEBUG + public bool UseSteamMatchmaking; + public bool RequireSteamAuthentication; + public bool AutomaticQuickStartEnabled; + public bool AutomaticCampaignLoadEnabled; + public bool TestScreenEnabled; + public bool TextManagerDebugModeEnabled; + public bool ModBreakerMode; +#else + public bool UseSteamMatchmaking => true; + public bool RequireSteamAuthentication => true; +#endif + + public struct GraphicsSettings + { + public static GraphicsSettings GetDefault() + { + GraphicsSettings gfxSettings = new GraphicsSettings + { + RadialDistortion = true, + InventoryScale = 1.0f, + LightMapScale = 1.0f, + TextScale = 1.0f, + HUDScale = 1.0f, + Specularity = true, + ChromaticAberration = true, + ParticleLimit = 1500, + LosMode = LosMode.Transparent + }; + gfxSettings.RadialDistortion = true; + gfxSettings.CompressTextures = true; + gfxSettings.FrameLimit = 300; + gfxSettings.VSync = true; +#if DEBUG + gfxSettings.DisplayMode = WindowMode.Windowed; +#else + gfxSettings.DisplayMode = WindowMode.BorderlessWindowed; +#endif + return gfxSettings; + } + + public static GraphicsSettings FromElements(IEnumerable elements, in GraphicsSettings? fallback = null) + { + GraphicsSettings retVal = fallback ?? GetDefault(); + elements.ForEach(element => retVal.DeserializeElement(element)); + return retVal; + } + + public int Width; + public int Height; + public bool VSync; + public bool CompressTextures; + public int FrameLimit; + public WindowMode DisplayMode; + public int ParticleLimit; + public bool Specularity; + public bool ChromaticAberration; + public LosMode LosMode; + public float HUDScale; + public float InventoryScale; + public float LightMapScale; + public float TextScale; + public bool RadialDistortion; + } + + [StructSerialization.Skip] + public GraphicsSettings Graphics; + + public struct AudioSettings + { + public static class DeviceNameHandler + { + public static string Read(string s) + => System.Xml.XmlConvert.DecodeName(s)!; + + public static string Write(string s) + => System.Xml.XmlConvert.EncodeName(s)!; + } + + public static AudioSettings GetDefault() + { + AudioSettings audioSettings = new AudioSettings + { + MusicVolume = 0.3f, + SoundVolume = 0.5f, + VoiceChatVolume = 0.5f, + VoiceChatCutoffPrevention = 0, + MicrophoneVolume = 5, + MuteOnFocusLost = false, + DynamicRangeCompressionEnabled = true, + UseDirectionalVoiceChat = true, + VoipAttenuationEnabled = true, + VoiceSetting = VoiceMode.PushToTalk, + UseLocalVoiceByDefault = false, + DisableVoiceChatFilters = false + }; + return audioSettings; + } + + public static AudioSettings FromElements(IEnumerable elements, in AudioSettings? fallback = null) + { + AudioSettings retVal = fallback ?? GetDefault(); + elements.ForEach(element => retVal.DeserializeElement(element)); + return retVal; + } + + public float MusicVolume; + public float SoundVolume; + public float VoiceChatVolume; + public int VoiceChatCutoffPrevention; + public float MicrophoneVolume; + public bool MuteOnFocusLost; + public bool DynamicRangeCompressionEnabled; + public bool UseDirectionalVoiceChat; + public bool VoipAttenuationEnabled; + public VoiceMode VoiceSetting; + + [StructSerialization.Handler(typeof(DeviceNameHandler))] + public string AudioOutputDevice; + [StructSerialization.Handler(typeof(DeviceNameHandler))] + public string VoiceCaptureDevice; + + public float NoiseGateThreshold; + public bool UseLocalVoiceByDefault; + public bool DisableVoiceChatFilters; + } + + [StructSerialization.Skip] + public AudioSettings Audio; + +#if CLIENT + public struct KeyMapping + { + private readonly static ImmutableDictionary DefaultsQwerty = + new Dictionary() + { + { InputType.Run, Keys.LeftShift }, + { InputType.Attack, Keys.R }, + { InputType.Crouch, Keys.LeftControl }, + { InputType.Grab, Keys.G }, + { InputType.Health, Keys.H }, + { InputType.Ragdoll, Keys.Space }, + { InputType.Aim, MouseButton.SecondaryMouse }, + + { InputType.InfoTab, Keys.Tab }, + { InputType.Chat, Keys.T }, + { InputType.RadioChat, Keys.R }, + { InputType.CrewOrders, Keys.C }, + + { InputType.Voice, Keys.V }, + { InputType.LocalVoice, Keys.B }, + { InputType.Command, MouseButton.MiddleMouse }, + { InputType.PreviousFireMode, MouseButton.MouseWheelDown }, + { InputType.NextFireMode, MouseButton.MouseWheelUp }, + + { InputType.TakeHalfFromInventorySlot, Keys.LeftShift }, + { InputType.TakeOneFromInventorySlot, Keys.LeftControl }, + + { InputType.Up, Keys.W }, + { InputType.Down, Keys.S }, + { InputType.Left, Keys.A }, + { InputType.Right, Keys.D }, + { InputType.ToggleInventory, Keys.Q }, + + { InputType.SelectNextCharacter, Keys.Z }, + { InputType.SelectPreviousCharacter, Keys.X }, + + { InputType.Use, Keys.E }, + { InputType.Select, MouseButton.PrimaryMouse }, + { InputType.Deselect, MouseButton.SecondaryMouse }, + { InputType.Shoot, MouseButton.PrimaryMouse } + }.ToImmutableDictionary(); + + public static KeyMapping GetDefault() => new KeyMapping + { + Bindings = DefaultsQwerty + .Select(kvp => + (kvp.Key, kvp.Value.MouseButton == MouseButton.None + ? (KeyOrMouse)Keyboard.QwertyToCurrentLayout(kvp.Value.Key) + : (KeyOrMouse)kvp.Value.MouseButton)) + .ToImmutableDictionary() + }; + + public KeyMapping(IEnumerable elements, in KeyMapping? fallback) + { + var defaultBindings = GetDefault().Bindings; + Dictionary bindings = fallback?.Bindings?.ToMutable() ?? defaultBindings.ToMutable(); + foreach (InputType inputType in (InputType[])Enum.GetValues(typeof(InputType))) + { + if (!bindings.ContainsKey(inputType)) { bindings.Add(inputType, defaultBindings[inputType]); } + } + + foreach (XElement element in elements) + { + foreach (XAttribute attribute in element.Attributes()) + { + if (Enum.TryParse(attribute.Name.LocalName, out InputType result)) + { + bindings[result] = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]); + } + } + } + + Bindings = bindings.ToImmutableDictionary(); + } + + public KeyMapping WithBinding(InputType type, KeyOrMouse bind) + { + KeyMapping newMapping = this; + newMapping.Bindings = newMapping.Bindings + .Select(kvp => + kvp.Key == type + ? (type, bind) + : (kvp.Key, kvp.Value)) + .ToImmutableDictionary(); + return newMapping; + } + + public ImmutableDictionary Bindings; + + public LocalizedString KeyBindText(InputType inputType) => Bindings[inputType].Name; + } + + [StructSerialization.Skip] + public KeyMapping KeyMap; + + public struct InventoryKeyMapping + { + public ImmutableArray Bindings; + + public static InventoryKeyMapping GetDefault() => new InventoryKeyMapping + { + Bindings = new KeyOrMouse[] + { + Keys.D1, + Keys.D2, + Keys.D3, + Keys.D4, + Keys.D5, + Keys.D6, + Keys.D7, + Keys.D8, + Keys.D9, + Keys.D0, + }.ToImmutableArray() + }; + + public InventoryKeyMapping WithBinding(int index, KeyOrMouse keyOrMouse) + { + var thisBindings = Bindings; + return new InventoryKeyMapping() + { + Bindings = Enumerable.Range(0, thisBindings.Length) + .Select(i => i == index ? keyOrMouse : thisBindings[i]) + .ToImmutableArray() + }; + } + + public InventoryKeyMapping(IEnumerable elements, InventoryKeyMapping? fallback) + { + var bindings = (fallback?.Bindings ?? GetDefault().Bindings).ToArray(); + foreach (XElement element in elements) + { + for (int i = 0; i < bindings.Length; i++) + { + bindings[i] = element.GetAttributeKeyOrMouse($"slot{i}", bindings[i]); + } + } + Bindings = bindings.ToImmutableArray(); + } + } + + [StructSerialization.Skip] + public InventoryKeyMapping InventoryKeyMap; +#endif + } + + public const string PlayerConfigPath = "config_player.xml"; + + private static Config currentConfig; + public static ref readonly Config CurrentConfig => ref currentConfig; + + public static void Init() + { + XDocument? currentConfigDoc = null; + + if (File.Exists(PlayerConfigPath)) + { + currentConfigDoc = XMLExtensions.TryLoadXml(PlayerConfigPath); + } + + if (currentConfigDoc != null) + { + currentConfig = Config.FromElement(currentConfigDoc.Root ?? throw new NullReferenceException("Config XML element is invalid: document is null.")); +#if CLIENT + ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); + MultiplayerPreferences.Init( + currentConfigDoc.Root.GetChildElement("player"), + currentConfigDoc.Root.GetChildElement("gameplay")?.GetChildElement("jobpreferences")); + IgnoredHints.Init(currentConfigDoc.Root.GetChildElement("ignoredhints")); + DebugConsoleMapping.Init(currentConfigDoc.Root.GetChildElement("debugconsolemapping")); + CompletedTutorials.Init(currentConfigDoc.Root.GetChildElement("tutorials")); +#endif + } + else + { + currentConfig = Config.GetDefault(); + SaveCurrentConfig(); + } + } + + public static void SetCurrentConfig(in Config newConfig) + { + bool setGraphicsMode = + currentConfig.Graphics.Width != newConfig.Graphics.Width + || currentConfig.Graphics.Height != newConfig.Graphics.Height + || currentConfig.Graphics.VSync != newConfig.Graphics.VSync + || currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; + + bool languageChanged = currentConfig.Language != newConfig.Language; + + currentConfig = newConfig; +#warning TODO: Implement program state updates; + +#if CLIENT + if (setGraphicsMode) + { + GameMain.Instance.ApplyGraphicsSettings(); + } + + GameMain.SoundManager?.ApplySettings(); +#endif + if (languageChanged) { TextManager.ClearCache(); } + } + + public static void SaveCurrentConfig() + { + XDocument configDoc = new XDocument(); + XElement root = new XElement("config"); configDoc.Add(root); + currentConfig.SerializeElement(root); + + XElement graphicsElement = new XElement("graphicssettings"); root.Add(graphicsElement); + currentConfig.Graphics.SerializeElement(graphicsElement); + +#region Backwards compatibility crap +#warning TODO: remove once modding refactor ships in a stable release + XElement backwardsCompatibilityGraphicsMode = new XElement(graphicsElement); root.Add(backwardsCompatibilityGraphicsMode); + backwardsCompatibilityGraphicsMode.Name = "graphicsmode"; +#endregion + + XElement audioElement = new XElement("audio"); root.Add(audioElement); + currentConfig.Audio.SerializeElement(audioElement); + + XElement contentPackagesElement = new XElement("contentpackages"); root.Add(contentPackagesElement); +#region More backwards compatibility crap + XComment backwardsCompatibleComment = new XComment("Backwards compatibility"); contentPackagesElement.Add(backwardsCompatibleComment); +#warning TODO: remove once modding refactor ships in a stable release + XElement backwardsCompatibleCoreElement = new XElement("core"); contentPackagesElement.Add(backwardsCompatibleCoreElement); + backwardsCompatibleCoreElement.SetAttributeValue("name", "Vanilla 0.9"); +#endregion + XComment corePackageComment = new XComment(ContentPackageManager.EnabledPackages.Core?.Name ?? "Vanilla"); contentPackagesElement.Add(corePackageComment); + XElement corePackageElement = new XElement(ContentPackageManager.CorePackageElementName); contentPackagesElement.Add(corePackageElement); + corePackageElement.SetAttributeValue("path", ContentPackageManager.EnabledPackages.Core?.Path ?? ContentPackageManager.VanillaFileList); + + XElement regularPackagesElement = new XElement(ContentPackageManager.RegularPackagesElementName); contentPackagesElement.Add(regularPackagesElement); + foreach (var regularPackage in ContentPackageManager.EnabledPackages.Regular) + { + XComment packageComment = new XComment(regularPackage.Name); regularPackagesElement.Add(packageComment); + XElement packageElement = new XElement(ContentPackageManager.RegularPackagesSubElementName); regularPackagesElement.Add(packageElement); + packageElement.SetAttributeValue("path", regularPackage.Path); + } + +#if CLIENT + XElement serverFiltersElement = new XElement("serverfilters"); root.Add(serverFiltersElement); + ServerListFilters.Instance.SaveTo(serverFiltersElement); + + XElement characterElement = new XElement("player"); root.Add(characterElement); + MultiplayerPreferences.Instance.SaveTo(characterElement); + + XElement ignoredHintsElement = new XElement("ignoredhints"); root.Add(ignoredHintsElement); + IgnoredHints.Instance.SaveTo(ignoredHintsElement); + + XElement debugConsoleMappingElement = new XElement("debugconsolemapping"); root.Add(debugConsoleMappingElement); + DebugConsoleMapping.Instance.SaveTo(debugConsoleMappingElement); + + XElement tutorialsElement = new XElement("tutorials"); root.Add(tutorialsElement); + CompletedTutorials.Instance.SaveTo(tutorialsElement); + + XElement keyMappingElement = new XElement("keymapping", + currentConfig.KeyMap.Bindings.Select(kvp + => new XAttribute(kvp.Key.ToString(), kvp.Value.ToString()))); + root.Add(keyMappingElement); + + XElement inventoryKeyMappingElement = new XElement("inventorykeymapping", + Enumerable.Range(0, currentConfig.InventoryKeyMap.Bindings.Length) + .Zip(currentConfig.InventoryKeyMap.Bindings) + .Cast<(int Index, KeyOrMouse Bind)>() + .Select(kvp + => new XAttribute($"slot{kvp.Index.ToString(CultureInfo.InvariantCulture)}", kvp.Bind.ToString()))); + root.Add(inventoryKeyMappingElement); +#endif + + configDoc.SaveSafe(PlayerConfigPath); + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + + try + { + using (var writer = XmlWriter.Create(PlayerConfigPath, settings)) + { + configDoc.WriteTo(writer); + writer.Flush(); + } + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving game settings failed.", e); + GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, + "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index b1f25b2c0..9cc87f0cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -17,7 +17,7 @@ namespace Barotrauma public DeformableSprite DeformableSprite { get; private set; } public Sprite ActiveSprite => Sprite ?? DeformableSprite.Sprite; - public ConditionalSprite(XElement element, ISerializableEntity target, string file = "", bool lazyLoad = false) + public ConditionalSprite(ContentXElement element, ISerializableEntity target, string file = "", bool lazyLoad = false) { Target = target; Exclusive = element.GetAttributeBool("exclusive", Exclusive); @@ -26,7 +26,7 @@ namespace Barotrauma { Enum.TryParse(comparison, ignoreCase: true, out Comparison); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs index 7fcc37437..502cfa9e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs @@ -18,7 +18,7 @@ namespace Barotrauma public Sprite Sprite { get; private set; } - public DeformableSprite(XElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false) + public DeformableSprite(ContentXElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false) { Sprite = new Sprite(element, file: filePath, lazyLoad: lazyLoad); InitProjSpecific(element, subdivisionsX, subdivisionsY, lazyLoad, invert); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 4cbd51d6c..55e75b67d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -39,7 +39,7 @@ namespace Barotrauma /// /// Reference to the xml element from where the sprite was created. Can be null if the sprite was not defined in xml! /// - public XElement SourceElement { get; private set; } + public ContentXElement SourceElement { get; private set; } //the area in the texture that is supposed to be drawn private Rectangle sourceRect; @@ -108,9 +108,9 @@ namespace Barotrauma public Vector2 RelativeSize { get; private set; } - public string FilePath { get; private set; } + public ContentPath FilePath { get; private set; } - public string FullPath { get; private set; } + public string FullPath => FilePath.FullPath; public bool Compress { get; private set; } @@ -119,11 +119,11 @@ namespace Barotrauma return FilePath + ": " + sourceRect; } - public string ID { get; private set; } + public Identifier Identifier { get; private set; } /// - /// ID of the Map Entity so that we can link the sprite to it's owner. + /// Identifier of the Map Entity so that we can link the sprite to its owner. /// - public string EntityID { get; set; } + public Identifier EntityIdentifier { get; set; } public string Name { get; set; } partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn); @@ -138,9 +138,9 @@ namespace Barotrauma } } - public Sprite(XElement element, string path = "", string file = "", bool lazyLoad = false) + public Sprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false) { - if (element == null) { return; } + if (element is null) { return; } this.LazyLoad = lazyLoad; SourceElement = element; if (!ParseTexturePath(path, file)) { return; } @@ -171,7 +171,7 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); - ID = GetID(SourceElement); + Identifier = GetIdentifier(SourceElement); AddToList(this); } @@ -201,11 +201,7 @@ namespace Barotrauma private void Init(string newFile, Rectangle? sourceRectangle = null, Vector2? newOrigin = null, Vector2? newOffset = null, float newRotation = 0) { - FilePath = newFile; - if (!string.IsNullOrEmpty(FilePath)) - { - FullPath = Path.GetFullPath(FilePath); - } + FilePath = ContentPath.FromRaw(newFile); Vector4 sourceVector = Vector4.Zero; bool shouldReturn = false; LoadTexture(ref sourceVector, ref shouldReturn); @@ -228,14 +224,15 @@ namespace Barotrauma } /// - /// Creates a supposedly unique id from the parent element. If the parent element is not found, uses the sprite element. + /// Creates a supposedly unique identifier from the parent element. If the parent element is not found, uses the sprite element. /// TODO: If there are multiple elements with exactly the same data, the ids will fail. -> Is there a better way to identify the sprites? + /// ALSO TODO: delete :) /// - public static string GetID(XElement sourceElement) + public static Identifier GetIdentifier(XElement sourceElement) { - if (sourceElement == null) { return string.Empty; } + if (sourceElement == null) { return "".ToIdentifier(); } var parentElement = sourceElement.Parent; - return parentElement != null ? sourceElement.ToString() + parentElement.ToString() : sourceElement.ToString(); + return $"{sourceElement}{parentElement?.ToString() ?? ""}".ToIdentifier(); } public void Remove() @@ -268,13 +265,13 @@ namespace Barotrauma } var doc = XMLExtensions.TryLoadXml(path); if (doc == null) { return; } - if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(EntityID)) { return; } + if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(EntityIdentifier.Value)) { return; } var spriteElements = doc.Descendants("sprite").Concat(doc.Descendants("Sprite")); var sourceElements = spriteElements.Where(e => e.GetAttributeString("name", null) == Name); if (sourceElements.None()) { // Try parents by first comparing the entity id and then the name, if no match was found. - sourceElements = spriteElements.Where(e => e.Parent?.GetAttributeString("identifier", null) == EntityID); + sourceElements = spriteElements.Where(e => e.Parent?.GetAttributeString("identifier", null) == EntityIdentifier); if (sourceElements.None()) { sourceElements = spriteElements.Where(e => e.Parent?.GetAttributeString("name", null) == Name); @@ -282,15 +279,15 @@ namespace Barotrauma } if (sourceElements.Multiple()) { - DebugConsole.NewMessage($"[Sprite] Multiple matching elements found by name ({Name}) or identifier ({EntityID})!: {SourceElement.ToString()}", Color.Yellow); + DebugConsole.NewMessage($"[Sprite] Multiple matching elements found by name ({Name}) or identifier ({EntityIdentifier})!: {SourceElement}", Color.Yellow); } else if (sourceElements.None()) { - DebugConsole.NewMessage($"[Sprite] Cannot find matching source element by comparing the name attribute ({Name}) or identifier ({EntityID})! Cannot reload the xml for sprite element \"{SourceElement.ToString()}\"!", Color.Yellow); + DebugConsole.NewMessage($"[Sprite] Cannot find matching source element by comparing the name attribute ({Name}) or identifier ({EntityIdentifier})! Cannot reload the xml for sprite element \"{SourceElement.ToString()}\"!", Color.Yellow); } else { - SourceElement = sourceElements.Single(); + SourceElement = sourceElements.Single().FromPackage(SourceElement.ContentPackage); } if (SourceElement != null) { @@ -311,7 +308,7 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); - ID = GetID(SourceElement); + Identifier = GetIdentifier(SourceElement); } } @@ -319,11 +316,11 @@ namespace Barotrauma { if (file == "") { - file = SourceElement.GetAttributeString("texture", ""); + file = SourceElement.GetAttributeStringUnrestricted("texture", ""); var overrideElement = GetLocalizationOverrideElement(); if (overrideElement != null) { - string overrideFile = overrideElement.GetAttributeString("texture", ""); + string overrideFile = overrideElement.GetAttributeStringUnrestricted("texture", ""); if (!string.IsNullOrEmpty(overrideFile)) { file = overrideFile; } } } @@ -336,22 +333,18 @@ namespace Barotrauma { if (!path.EndsWith("/")) path += "/"; } - FilePath = (path + file).CleanUpPathCrossPlatform(correctFilenameCase: true); - if (!string.IsNullOrEmpty(FilePath)) - { - FullPath = Path.GetFullPath(FilePath); - } + FilePath = ContentPath.FromRaw(SourceElement.ContentPackage, (path + file).CleanUpPathCrossPlatform(correctFilenameCase: true)); return true; } private XElement GetLocalizationOverrideElement() { - foreach (XElement subElement in SourceElement.Elements()) + foreach (var subElement in SourceElement.Elements()) { if (subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { - string language = subElement.GetAttributeString("language", ""); - if (TextManager.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase)) + LanguageIdentifier language = subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier(); + if (GameSettings.CurrentConfig.Language == language) { return subElement; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs index a186f716a..b9164a09a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs @@ -19,8 +19,8 @@ namespace Barotrauma get; private set; } - - public SpriteSheet(XElement element, string path = "", string file = "") + + public SpriteSheet(ContentXElement element, string path = "", string file = "") : base(element, path, file) { int columnCount = Math.Max(element.GetAttributeInt("columns", 1), 1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index e6d051abf..025127e42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -36,7 +36,7 @@ namespace Barotrauma private readonly DelayTypes delayType; private readonly float delay; - public DelayedEffect(XElement element, string parentDebugName) + public DelayedEffect(ContentXElement element, string parentDebugName) : base(element, parentDebugName) { string delayTypeStr = element.GetAttributeString("delaytype", "timer"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index b8ca1a030..bd86e6618 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -15,12 +15,14 @@ namespace Barotrauma { public enum ConditionType { + Uncertain, PropertyValue, Name, SpeciesName, SpeciesGroup, HasTag, HasStatusTag, + HasSpecifierTag, Affliction, EntityType, LimbType @@ -34,18 +36,18 @@ namespace Barotrauma public enum OperatorType { + None, Equals, NotEquals, LessThan, LessThanEquals, GreaterThan, - GreaterThanEquals, - None + GreaterThanEquals } public readonly ConditionType Type; public readonly OperatorType Operator; - public readonly string AttributeName; + public readonly Identifier AttributeName; public readonly string AttributeValue; public readonly string[] SplitAttributeValue; public readonly float? FloatValue; @@ -79,35 +81,22 @@ namespace Barotrauma // TODO: use XElement instead of XAttribute (how to do without breaking the existing content?) public PropertyConditional(XAttribute attribute) { - AttributeName = attribute.Name.ToString().ToLowerInvariant(); - string attributeValueString = attribute.Value.ToString(); + AttributeName = attribute.NameAsIdentifier(); + string attributeValueString = attribute.Value; if (string.IsNullOrWhiteSpace(attributeValueString)) { - DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent.ToString()}"); + DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent}"); return; } string valueString = attributeValueString; - string[] splitString = valueString.Split(' '); - if (splitString.Length > 0) - { - for (int i = 1; i < splitString.Length; i++) - { - valueString = splitString[i] + (i > 1 && i < splitString.Length ? " " : ""); - } - } - string op = splitString[0]; - OperatorType operatorType = GetOperatorType(op); + string[] splitString = valueString.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (splitString.Length > 1) { valueString = string.Join(' ', splitString.Skip(1)); } + Operator = GetOperatorType(splitString[0]); - if (operatorType != OperatorType.None) + if (Operator == OperatorType.None) { - Operator = operatorType; - } - else - { - if (op != "==" && op != "!=" && op != ">" && op != "<" && op != ">=" && op != "<=") //Didn't use escape strings or anything - { - valueString = attributeValueString; //We probably don't even have an operator - } + Operator = OperatorType.Equals; + valueString = attributeValueString; } TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); @@ -116,16 +105,9 @@ namespace Barotrauma TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); - if (!Enum.TryParse(AttributeName, true, out Type)) + if (!Enum.TryParse(AttributeName.Value, true, out Type)) { - if (AfflictionPrefab.Prefabs.Any(p => p.Identifier.Equals(AttributeName, StringComparison.OrdinalIgnoreCase))) - { - Type = ConditionType.Affliction; - } - else - { - Type = ConditionType.PropertyValue; - } + Type = ConditionType.Uncertain; } AttributeValue = valueString; @@ -172,15 +154,36 @@ namespace Barotrauma } } + public bool Matches(ISerializableEntity target) - { - if (TargetContainedItem) + { + return Matches(target, TargetContainedItem); + } + + public bool Matches(ISerializableEntity target, bool checkContained) + { + var type = Type; + if (type == ConditionType.Uncertain) + { + if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName)) + { + type = ConditionType.Affliction; + } + else + { + type = (target?.SerializableProperties?.ContainsKey(AttributeName) ?? false) + ? ConditionType.PropertyValue + : ConditionType.HasSpecifierTag; + } + } + + if (checkContained) { if (target is Item item) { foreach (var containedItem in item.ContainedItems) { - if (Matches(containedItem)) { return true; } + if (Matches(containedItem, checkContained: false)) { return true; } } return false; } @@ -188,7 +191,7 @@ namespace Barotrauma { foreach (var containedItem in ic.Item.ContainedItems) { - if (Matches(containedItem)) { return true; } + if (Matches(containedItem, checkContained: false)) { return true; } } return false; } @@ -197,13 +200,13 @@ namespace Barotrauma if (character.Inventory == null) { return false; } foreach (var containedItem in character.Inventory.AllItems) { - if (Matches(containedItem)) { return true; } + if (Matches(containedItem, checkContained: false)) { return true; } } return false; } } - switch (Type) + switch (type) { case ConditionType.PropertyValue: SerializableProperty property; @@ -244,18 +247,18 @@ namespace Barotrauma } } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; case ConditionType.SpeciesName: { if (target == null) { return Operator == OperatorType.NotEquals; } if (!(target is Character targetCharacter)) { return false; } - return Operator == OperatorType.Equals == targetCharacter.SpeciesName.Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); + return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == AttributeValue); } case ConditionType.SpeciesGroup: { if (target == null) { return Operator == OperatorType.NotEquals; } if (!(target is Character targetCharacter)) { return false; } - return Operator == OperatorType.Equals == targetCharacter.Params.CompareGroup(AttributeValue); + return (Operator == OperatorType.Equals) == targetCharacter.Params.CompareGroup(AttributeValue.ToIdentifier()); } case ConditionType.EntityType: switch (AttributeValue) @@ -298,7 +301,7 @@ namespace Barotrauma { var health = targetChar.CharacterHealth; if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName); + var affliction = health.GetAffliction(AttributeName.ToIdentifier()); float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; if (FloatValue.HasValue) { @@ -343,14 +346,14 @@ namespace Barotrauma return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - public bool MatchesTagCondition(string targetTag) + public bool MatchesTagCondition(Identifier targetTag) { - if (string.IsNullOrEmpty(targetTag) || Type != ConditionType.HasTag) { return false; } + if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; } int matches = 0; foreach (string tag in SplitAttributeValue) { - if (targetTag.Equals(tag, StringComparison.OrdinalIgnoreCase)) + if (targetTag == tag) { matches++; } @@ -358,7 +361,7 @@ namespace Barotrauma //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - + // TODO: refactor and add tests private bool Matches(ISerializableEntity target, SerializableProperty property) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index f4d193768..bc1da8b5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -5,6 +5,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -44,24 +45,24 @@ namespace Barotrauma { public string Name => "ai trigger"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } - [Serialize(AIState.Idle, false)] + [Serialize(AIState.Idle, IsPropertySaveable.No)] public AIState State { get; private set; } - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float Duration { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float Probability { get; private set; } - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float MinDamage { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool AllowToOverride { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool AllowToBeOverridden { get; private set; } public bool IsTriggered { get; private set; } @@ -152,6 +153,8 @@ namespace Barotrauma public readonly float AimSpread; public readonly bool Equip; + public readonly float Condition; + public ItemSpawnInfo(XElement element, string parentDebugName) { if (element.Attribute("name") != null) @@ -185,6 +188,8 @@ namespace Barotrauma SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); Speed = element.GetAttributeFloat("speed", 0.0f); + Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f); + Rotation = element.GetAttributeFloat("rotation", 0.0f); Count = element.GetAttributeInt("count", 1); Spread = element.GetAttributeFloat("spread", 0f); @@ -206,36 +211,36 @@ namespace Barotrauma public class AbilityStatusEffectIdentifier : AbilityObject { - public AbilityStatusEffectIdentifier(string effectIdentifier) + public AbilityStatusEffectIdentifier(Identifier effectIdentifier) { EffectIdentifier = effectIdentifier; } - public string EffectIdentifier { get; set; } + public Identifier EffectIdentifier { get; set; } } public class GiveTalentInfo { - public string[] TalentIdentifiers; + public Identifier[] TalentIdentifiers; public bool GiveRandom; public GiveTalentInfo(XElement element, string _) { - TalentIdentifiers = element.GetAttributeStringArray("talentidentifiers", new string[0], convertToLowerInvariant: true); + TalentIdentifiers = element.GetAttributeIdentifierArray("talentidentifiers", Array.Empty()); GiveRandom = element.GetAttributeBool("giverandom", false); } } public class GiveSkill { - public string SkillIdentifier; - public float Amount; + public readonly Identifier SkillIdentifier; + public readonly float Amount; public GiveSkill(XElement element, string parentDebugName) { - SkillIdentifier = element.GetAttributeString("skillidentifier", string.Empty); + SkillIdentifier = element.GetAttributeIdentifier("skillidentifier", Identifier.Empty); Amount = element.GetAttributeFloat("amount", 0); - if (SkillIdentifier == string.Empty) + if (SkillIdentifier == Identifier.Empty) { DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!"); } @@ -245,24 +250,24 @@ namespace Barotrauma public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } - [Serialize("", false)] - public string SpeciesName { get; private set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier SpeciesName { get; private set; } - [Serialize(1, false)] + [Serialize(1, IsPropertySaveable.No)] public int Count { get; private set; } - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float Spread { get; private set; } - [Serialize("0,0", false)] + [Serialize("0,0", IsPropertySaveable.No)] public Vector2 Offset { get; private set; } public CharacterSpawnInfo(XElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - if (string.IsNullOrEmpty(SpeciesName)) + if (SpeciesName.IsEmpty) { DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\""); } @@ -270,7 +275,6 @@ namespace Barotrauma } private readonly TargetType targetTypes; - protected HashSet targetIdentifiers; /// /// Index of the slot the target must be in when targeting a Contained item @@ -279,7 +283,7 @@ namespace Barotrauma private readonly List requiredItems; - public readonly string[] propertyNames; + public readonly Identifier[] propertyNames; public readonly object[] propertyEffects; private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; @@ -324,8 +328,8 @@ namespace Barotrauma private readonly List aiTriggers; private readonly List triggeredEvents; - private readonly string triggeredEventTargetTag = "statuseffecttarget", - triggeredEventEntityTag = "statuseffectentity"; + private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(), + triggeredEventEntityTag = "statuseffectentity".ToIdentifier(); private Character user; @@ -347,15 +351,12 @@ namespace Barotrauma /// public readonly bool AllowWhenBroken = false; - public HashSet TargetIdentifiers - { - get { return targetIdentifiers; } - } + public readonly ImmutableHashSet TargetIdentifiers; /// /// Which type of afflictions the target must receive for the StatusEffect to be applied. Only valid when the type of the effect is OnDamaged. /// - private readonly HashSet<(string affliction, float strength)> requiredAfflictions; + private readonly HashSet<(Identifier affliction, float strength)> requiredAfflictions; public float AfflictionMultiplier = 1.0f; @@ -372,9 +373,9 @@ namespace Barotrauma get { return spawnCharacters; } } - public readonly List<(string affliction, float amount)> ReduceAffliction; + public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction; - private readonly List talentTriggers; + private readonly List talentTriggers; private readonly List giveExperiences; private readonly List giveSkills; @@ -406,7 +407,7 @@ namespace Barotrauma } } - public static StatusEffect Load(XElement element, string parentDebugName) + public static StatusEffect Load(ContentXElement element, string parentDebugName) { if (element.Attribute("delay") != null || element.Attribute("delaytype") != null) { @@ -416,7 +417,7 @@ namespace Barotrauma return new StatusEffect(element, parentDebugName); } - protected StatusEffect(XElement element, string parentDebugName) + protected StatusEffect(ContentXElement element, string parentDebugName) { requiredItems = new List(); spawnItems = new List(); @@ -427,8 +428,8 @@ namespace Barotrauma Afflictions = new List(); Explosions = new List(); triggeredEvents = new List(); - ReduceAffliction = new List<(string affliction, float amount)>(); - talentTriggers = new List(); + ReduceAffliction = new List<(Identifier affliction, float amount)>(); + talentTriggers = new List(); giveExperiences = new List(); giveSkills = new List(); multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); @@ -460,7 +461,7 @@ namespace Barotrauma string[] targetTypesStr = element.GetAttributeStringArray("target", null) ?? - element.GetAttributeStringArray("targettype", new string[0]); + element.GetAttributeStringArray("targettype", Array.Empty()); foreach (string s in targetTypesStr) { if (!Enum.TryParse(s, true, out TargetType targetType)) @@ -500,20 +501,15 @@ namespace Barotrauma case "targets": case "targetidentifiers": case "targettags": - string[] identifiers = attribute.Value.Split(','); - targetIdentifiers = new HashSet(); - for (int i = 0; i < identifiers.Length; i++) - { - targetIdentifiers.Add(identifiers[i].Trim().ToLowerInvariant()); - } + TargetIdentifiers = attribute.Value.Split(',').ToIdentifiers().ToImmutableHashSet(); break; case "allowedafflictions": case "requiredafflictions": string[] types = attribute.Value.Split(','); - requiredAfflictions ??= new HashSet<(string, float)>(); + requiredAfflictions ??= new HashSet<(Identifier, float)>(); for (int i = 0; i < types.Length; i++) { - requiredAfflictions.Add((types[i].Trim().ToLowerInvariant(), 0.0f)); + requiredAfflictions.Add((types[i].Trim().ToIdentifier(), 0.0f)); } break; case "duration": @@ -527,10 +523,10 @@ namespace Barotrauma lifeTimer = lifeTime; break; case "eventtargettag": - triggeredEventTargetTag = attribute.Value; + triggeredEventTargetTag = attribute.Value.ToIdentifier(); break; case "evententitytag": - triggeredEventEntityTag = attribute.Value; + triggeredEventEntityTag = attribute.Value.ToIdentifier(); break; case "checkconditionalalways": CheckConditionalAlways = attribute.GetAttributeBool(false); @@ -574,19 +570,19 @@ namespace Barotrauma int count = propertyAttributes.Count; - propertyNames = new string[count]; + propertyNames = new Identifier[count]; propertyEffects = new object[count]; int n = 0; foreach (XAttribute attribute in propertyAttributes) { - propertyNames[n] = attribute.Name.ToString().ToLowerInvariant(); + propertyNames[n] = attribute.NameAsIdentifier(); propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute); n++; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -625,9 +621,9 @@ namespace Barotrauma requiredItems.Add(newRequiredItem); break; case "requiredaffliction": - requiredAfflictions ??= new HashSet<(string, float)>(); - string[] ids = subElement.GetAttributeStringArray("identifier", null) ?? subElement.GetAttributeStringArray("type", new string[0]); - foreach (string afflictionId in ids) + requiredAfflictions ??= new HashSet<(Identifier, float)>(); + Identifier[] ids = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("type", Array.Empty()); + foreach (var afflictionId in ids) { requiredAfflictions.Add(( afflictionId, @@ -648,8 +644,8 @@ namespace Barotrauma if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names."); - string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.ToLowerInvariant() == afflictionName); + string afflictionName = subElement.GetAttributeString("name", ""); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); @@ -658,8 +654,8 @@ namespace Barotrauma } else { - string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); @@ -677,13 +673,12 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); ReduceAffliction.Add(( - subElement.GetAttributeString("name", "").ToLowerInvariant(), + subElement.GetAttributeIdentifier("name", ""), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } else { - string name = subElement.GetAttributeString("identifier", null) ?? subElement.GetAttributeString("type", null); - name = name.ToLowerInvariant(); + Identifier name = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty)); if (AfflictionPrefab.List.Any(ap => ap.Identifier == name || ap.AfflictionType == name)) { @@ -700,8 +695,8 @@ namespace Barotrauma if (newSpawnItem.ItemPrefab != null) { spawnItems.Add(newSpawnItem); } break; case "triggerevent": - string identifier = subElement.GetAttributeString("identifier", null); - if (!string.IsNullOrWhiteSpace(identifier)) + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (!identifier.IsEmpty) { EventPrefab prefab = EventSet.GetEventPrefab(identifier); if (prefab != null) @@ -709,15 +704,15 @@ namespace Barotrauma triggeredEvents.Add(prefab); } } - foreach (XElement eventElement in subElement.Elements()) + foreach (var eventElement in subElement.Elements()) { - if (!eventElement.Name.ToString().Equals("ScriptedEvent", StringComparison.OrdinalIgnoreCase)) { continue; } - triggeredEvents.Add(new EventPrefab(eventElement)); + if (eventElement.NameAsIdentifier() != "ScriptedEvent") { continue; } + triggeredEvents.Add(new EventPrefab(eventElement, file: null)); } break; case "spawncharacter": var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); - if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); } + if (!newSpawnCharacter.SpeciesName.IsEmpty) { spawnCharacters.Add(newSpawnCharacter); } break; case "givetalentinfo": var newGiveTalentInfo = new GiveTalentInfo(subElement, parentDebugName); @@ -727,7 +722,7 @@ namespace Barotrauma aiTriggers.Add(new AITrigger(subElement)); break; case "talenttrigger": - talentTriggers.Add(subElement.GetAttributeString("effectidentifier", string.Empty)); + talentTriggers.Add(subElement.GetAttributeIdentifier("effectidentifier", Identifier.Empty)); break; case "giveexperience": giveExperiences.Add(subElement.GetAttributeInt("amount", 0)); @@ -740,7 +735,7 @@ namespace Barotrauma InitProjSpecific(element, parentDebugName); } - partial void InitProjSpecific(XElement element, string parentDebugName); + partial void InitProjSpecific(ContentXElement element, string parentDebugName); public bool HasTargetType(TargetType targetType) { @@ -832,8 +827,8 @@ namespace Barotrauma if (HasTargetType(TargetType.NearbyItems)) { //optimization for powered components that can be easily fetched from Powered.PoweredList - if (targetIdentifiers?.Count == 1 && - (targetIdentifiers.Contains("powered") || targetIdentifiers.Contains("junctionbox") || targetIdentifiers.Contains("relaycomponent"))) + if (TargetIdentifiers.Count == 1 && + (TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent"))) { foreach (Powered powered in Powered.PoweredList) { @@ -1010,79 +1005,45 @@ namespace Barotrauma } else if (entity is Structure structure) { - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("structure")) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("structure")) { return true; } + if (TargetIdentifiers.Contains(structure.Prefab.Identifier)) { return true; } } else if (entity is Character character) { return IsValidTarget(character); } - if (targetIdentifiers == null) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + return TargetIdentifiers.Contains(entity.Name); } protected bool IsValidTarget(ItemComponent itemComponent) { if (OnlyInside && itemComponent.Item.CurrentHull == null) { return false; } if (OnlyOutside && itemComponent.Item.CurrentHull != null) { return false; } - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("itemcomponent")) { return true; } - if (itemComponent.Item.HasTag(targetIdentifiers)) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("itemcomponent")) { return true; } + if (itemComponent.Item.HasTag(TargetIdentifiers)) { return true; } + return TargetIdentifiers.Contains(itemComponent.Item.Prefab.Identifier); } protected bool IsValidTarget(Item item) { if (OnlyInside && item.CurrentHull == null) { return false; } if (OnlyOutside && item.CurrentHull != null) { return false; } - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("item")) { return true; } - if (item.HasTag(targetIdentifiers)) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("item")) { return true; } + if (item.HasTag(TargetIdentifiers)) { return true; } + return TargetIdentifiers.Contains(item.Prefab.Identifier); } protected bool IsValidTarget(Character character) { if (OnlyInside && character.CurrentHull == null) { return false; } if (OnlyOutside && character.CurrentHull != null) { return false; } - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("character")) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("character")) { return true; } + return TargetIdentifiers.Contains(character.SpeciesName); } public void SetUser(Character user) @@ -1129,7 +1090,7 @@ namespace Barotrauma currentTargets.Add(target); } - if (targetIdentifiers != null && currentTargets.Count == 0) { return; } + if (TargetIdentifiers != null && currentTargets.Count == 0) { return; } bool hasRequiredItems = HasRequiredItems(entity); if (!hasRequiredItems || !HasRequiredConditions(currentTargets)) @@ -1259,14 +1220,14 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } + if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } } } if (removeCharacter) { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } + if (targets[i] is Character character) { Entity.Spawner?.AddEntityToRemoveQueue(character); } } } if (breakLimb || hideLimb) @@ -1369,7 +1330,7 @@ namespace Barotrauma RegisterTreatmentResults(entity, limb, affliction, result); } } - + foreach (var (affliction, amount) in ReduceAffliction) { Limb targetLimb = null; @@ -1389,7 +1350,14 @@ namespace Barotrauma if (entity is Item item && item.UseInHealthInterface) { actionType = type; } float reduceAmount = amount * GetAfflictionMultiplier(entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + if (targetLimb != null) + { + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + } + else + { + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); + } targetCharacter.AIController?.OnHealed(healer: user, targetCharacter.Vitality - prevVitality); if (user != null && user != targetCharacter) { @@ -1435,7 +1403,7 @@ namespace Barotrauma Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) { - foreach (string talentTrigger in talentTriggers) + foreach (Identifier talentTrigger in talentTriggers) { targetCharacter.CheckTalents(AbilityEffectType.OnStatusEffectIdentifier, new AbilityStatusEffectIdentifier(talentTrigger)); } @@ -1461,13 +1429,13 @@ namespace Barotrauma Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) { - string skillIdentifier = giveSkill.SkillIdentifier.ToLowerInvariant() == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; + Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount); - string GetRandomSkill() + Identifier GetRandomSkill() { - return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandom(); + return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandomUnsynced() ?? Identifier.Empty; } } } @@ -1477,22 +1445,22 @@ namespace Barotrauma { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter?.Info == null) { continue; } - if (!TalentTree.JobTalentTrees.TryGetValue(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { continue; } + if (!TalentTree.JobTalentTrees.TryGet(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { continue; } // for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well - IEnumerable disallowedTalents = talentTree.TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))); + IEnumerable disallowedTalents = talentTree.TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))); foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos) { - IEnumerable viableTalents = giveTalentInfo.TalentIdentifiers.Where(s => !targetCharacter.Info.UnlockedTalents.Contains(s) && !disallowedTalents.Contains(s)); + IEnumerable viableTalents = giveTalentInfo.TalentIdentifiers.Where(s => !targetCharacter.Info.UnlockedTalents.Contains(s) && !disallowedTalents.Contains(s)); if (viableTalents.None()) { continue; } if (giveTalentInfo.GiveRandom) { - targetCharacter.GiveTalent(viableTalents.GetRandom(Rand.RandSync.Unsynced), true); + targetCharacter.GiveTalent(viableTalents.GetRandomUnsynced(), true); } else { - foreach (string talent in viableTalents) + foreach (Identifier talent in viableTalents) { targetCharacter.GiveTalent(talent, true); } @@ -1518,7 +1486,7 @@ namespace Barotrauma if (ev is ScriptedEvent scriptedEvent) { - if (!string.IsNullOrWhiteSpace(triggeredEventTargetTag)) + if (!triggeredEventTargetTag.IsEmpty) { List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); @@ -1528,7 +1496,7 @@ namespace Barotrauma } } - if (!string.IsNullOrWhiteSpace(triggeredEventEntityTag) && entity != null) + if (!triggeredEventEntityTag.IsEmpty && entity != null) { scriptedEvent.Targets.Add(triggeredEventEntityTag, new List { entity }); } @@ -1543,7 +1511,7 @@ namespace Barotrauma var characters = new List(); for (int i = 0; i < characterSpawnInfo.Count; i++) { - Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, + Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, onSpawn: newCharacter => { if (newCharacter.AIController is EnemyAIController enemyAi && @@ -1564,7 +1532,7 @@ namespace Barotrauma if (spawnItemRandomly) { - SpawnItem(spawnItems.GetRandom(Rand.RandSync.Unsynced)); + SpawnItem(spawnItems.GetRandomUnsynced()); } else { @@ -1583,7 +1551,7 @@ namespace Barotrauma switch (chosenItemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { Projectile projectile = newItem.GetComponent(); if (projectile != null && user != null && sourceBody != null && entity != null) @@ -1673,6 +1641,7 @@ namespace Barotrauma body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); } } + newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; }); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: @@ -1693,7 +1662,7 @@ namespace Barotrauma } if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) { - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => { if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) { @@ -1705,6 +1674,7 @@ namespace Barotrauma allowedSlots.Remove(InvSlotType.Any); character.Inventory.TryPutItem(item, null, allowedSlots); } + item.Condition = item.MaxCondition * chosenItemSpawnInfo.Condition; }); } } @@ -1732,7 +1702,10 @@ namespace Barotrauma Inventory containedInventory = item.GetComponent()?.Inventory; if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) { - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull); + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + }); } break; } @@ -1853,7 +1826,7 @@ namespace Barotrauma element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result); } } - + foreach (var (affliction, amount) in element.Parent.ReduceAffliction) { Limb targetLimb = null; @@ -1873,7 +1846,14 @@ namespace Barotrauma if (element.Entity is Item item && item.UseInHealthInterface) { actionType = element.Parent.type; } float reduceAmount = amount * element.Parent.GetAfflictionMultiplier(element.Entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + if (targetLimb != null) + { + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount * deltaTime, treatmentAction: actionType); + } + else + { + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount * deltaTime, treatmentAction: actionType); + } if (element.User != null && element.User != targetCharacter) { targetCharacter.AIController?.OnHealed(healer: element.User, targetCharacter.Vitality - prevVitality); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs new file mode 100644 index 000000000..94676dc7c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs @@ -0,0 +1,18 @@ +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + private static Steamworks.AuthTicket currentTicket = null; + public static Steamworks.AuthTicket GetAuthSessionTicket() + { + if (!IsInitialized) + { + return null; + } + + currentTicket?.Cancel(); + currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); + return currentTicket; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs similarity index 71% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs rename to Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 347af34f7..a3f7cb201 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -1,10 +1,7 @@ -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; -#if USE_STEAM namespace Barotrauma.Steam { static partial class SteamManager @@ -13,16 +10,6 @@ namespace Barotrauma.Steam public const uint AppID = 602960; - private static readonly List initializationErrors = new List(); - public static IEnumerable InitializationErrors - { - get { return initializationErrors; } - } - - public const string MetadataFileName = "filelist.xml"; - - public const string CopyIndicatorFileName = ".copying"; - private static readonly Dictionary tagCommonness = new Dictionary() { { "submarine", 10 }, @@ -37,18 +24,17 @@ namespace Barotrauma.Steam { "language", 5 } }; + public static bool IsInitialized { get; private set; } + private static readonly List popularTags = new List(); public static IEnumerable PopularTags { get { - if (!isInitialized) { return Enumerable.Empty(); } + if (!IsInitialized) { return Enumerable.Empty(); } return popularTags; } } - - private static bool isInitialized; - public static bool IsInitialized => isInitialized; public static void Initialize() { @@ -57,7 +43,7 @@ namespace Barotrauma.Steam public static ulong GetSteamID() { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0; } @@ -65,42 +51,35 @@ namespace Barotrauma.Steam return Steamworks.SteamClient.SteamId; } + public static bool IsFamilyShared() + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + + return Steamworks.SteamApps.IsSubscribedFromFamilySharing; + } + + public static bool IsFreeWeekend() + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + + return Steamworks.SteamApps.IsSubscribedFromFamilySharing; + } + public static string GetUsername() { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return ""; } return Steamworks.SteamClient.Name; } - private static Steamworks.AuthTicket currentTicket = null; - public static Steamworks.AuthTicket GetAuthSessionTicket() - { - if (!isInitialized) - { - return null; - } + public static bool UnlockAchievement(string achievementIdentifier) => + UnlockAchievement(achievementIdentifier.ToIdentifier()); - currentTicket?.Cancel(); - currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); - return currentTicket; - } - - public static bool OverlayCustomURL(string url) + public static bool UnlockAchievement(Identifier achievementIdentifier) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) - { - return false; - } - - Steamworks.SteamFriends.OpenWebOverlay(url); - return true; - } - - public static bool UnlockAchievement(string achievementIdentifier) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } @@ -116,24 +95,20 @@ namespace Barotrauma.Steam //SteamAchievementManager tries to unlock achievements that may or may not exist //(discovered[whateverbiomewasentered], kill[withwhateveritem], kill[somemonster] etc) so that we can add //some types of new achievements without the need for client-side changes. -#if DEBUG - DebugConsole.NewMessage("Failed to unlock achievement \"" + achievementIdentifier + "\"."); -#endif + DebugConsole.Log($"Failed to unlock achievement \"{achievementIdentifier}\"."); } return unlocked; } - public static bool IncrementStat(string statName, int increment) + public static bool IncrementStat(Identifier statName, int increment) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } - DebugConsole.Log("Incremented stat \"" + statName + "\" by " + increment); - bool success = Steamworks.SteamUserStats.AddStat(statName, increment); + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment); + bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment); if (!success) { -#if DEBUG - DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); -#endif + DebugConsole.Log("Failed to increment stat \"" + statName + "\"."); } else { @@ -142,16 +117,14 @@ namespace Barotrauma.Steam return success; } - public static bool IncrementStat(string statName, float increment) + public static bool IncrementStat(Identifier statName, float increment) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } - DebugConsole.Log("Incremented stat \"" + statName + "\" by " + increment); - bool success = Steamworks.SteamUserStats.AddStat(statName, increment); + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment); + bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment); if (!success) { -#if DEBUG - DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); -#endif + DebugConsole.Log("Failed to increment stat \"" + statName + "\"."); } else { @@ -160,29 +133,27 @@ namespace Barotrauma.Steam return success; } - public static int GetStatInt(string statName) + public static int GetStatInt(Identifier statName) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return 0; } - return Steamworks.SteamUserStats.GetStatInt(statName); + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0; } + return Steamworks.SteamUserStats.GetStatInt(statName.Value.ToLowerInvariant()); } public static bool StoreStats() { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } DebugConsole.Log("Storing Steam stats..."); bool success = Steamworks.SteamUserStats.StoreStats(); if (!success) { -#if DEBUG - DebugConsole.NewMessage("Failed to store Steam stats."); -#endif + DebugConsole.Log("Failed to store Steam stats."); } return success; } public static bool TryGetUnlockedAchievements(out List achievements) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { achievements = null; return false; @@ -193,7 +164,7 @@ namespace Barotrauma.Steam public static void Update(float deltaTime) { - if (!isInitialized) { return; } + if (!IsInitialized) { return; } if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.RunCallbacks(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.RunCallbacks(); } @@ -203,11 +174,11 @@ namespace Barotrauma.Steam public static void ShutDown() { - if (!isInitialized) { return; } + if (!IsInitialized) { return; } if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.Shutdown(); } - isInitialized = false; + IsInitialized = false; } public static IEnumerable ParseWorkshopIds(string workshopIdData) @@ -246,7 +217,7 @@ namespace Barotrauma.Steam try { Uri uri = new Uri(url); - string idStr = HttpUtility.ParseQueryString(uri.Query)["id"]; + string idStr = HttpUtility.ParseQueryString(uri.Query)["id".ToIdentifier()]; if (ulong.TryParse(idStr, out ulong id)) { return id; @@ -265,7 +236,7 @@ namespace Barotrauma.Steam if (string.IsNullOrWhiteSpace(str)) { return 0; } UInt64 retVal; if (str.StartsWith("STEAM64_", StringComparison.InvariantCultureIgnoreCase)) { str = str.Substring(8); } - if (UInt64.TryParse(str, out retVal) && retVal >(1<<52)) { return retVal; } + if (UInt64.TryParse(str, out retVal) && retVal > (1 << 52)) { return retVal; } if (!str.StartsWith("STEAM_", StringComparison.InvariantCultureIgnoreCase)) { return 0; } string[] split = str.Substring(6).Split(':'); if (split.Length != 3) { return 0; } @@ -293,4 +264,3 @@ namespace Barotrauma.Steam } } } -#endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs new file mode 100644 index 000000000..382b21282 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -0,0 +1,441 @@ +#nullable enable +using Barotrauma.IO; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Steamworks.Data; +using WorkshopItemSet = System.Collections.Generic.ISet; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + public const string WorkshopItemPreviewImageFolder = "Workshop"; + public const string PreviewImageName = "PreviewImage.png"; + public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; + + public static partial class Workshop + { + private struct ItemEqualityComparer : IEqualityComparer + { + public static readonly ItemEqualityComparer Instance = new ItemEqualityComparer(); + + public bool Equals(Steamworks.Ugc.Item x, Steamworks.Ugc.Item y) + => x.Id == y.Id; + + public int GetHashCode(Steamworks.Ugc.Item obj) + => (int)obj.Id.Value; + } + + private static async Task GetWorkshopItems(Steamworks.Ugc.Query query, int? maxPages = null) + { + if (!IsInitialized) { return new HashSet(); } + + await Task.Yield(); + query = query.WithKeyValueTags(true).WithLongDescription(true); + var set = new HashSet(ItemEqualityComparer.Instance); + int prevSize = 0; + for (int i = 1; maxPages is null || i <= maxPages; i++) + { + Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i); + if (page is null || !page.Value.Entries.Any()) { break; } + set.UnionWith(page.Value.Entries); + + if (set.Count == prevSize) { break; } + prevSize = set.Count; + } + return set; + } + + public static async Task GetAllSubscribedItems() + { + if (!IsInitialized) { return new HashSet(); } + + return await GetWorkshopItems( + Steamworks.Ugc.Query.Items + .WhereUserSubscribed()); + } + + public static async Task GetPopularItems() + { + if (!IsInitialized) { return new HashSet(); } + + return await GetWorkshopItems( + Steamworks.Ugc.Query.Items + .WithTrendDays(7) + .RankedByTrend(), maxPages: 1); + } + + public static async Task GetPublishedItems() + { + if (!IsInitialized) { return new HashSet(); } + + return await GetWorkshopItems( + Steamworks.Ugc.Query.All + .WhereUserPublished()); + } + + public static async Task GetItem(UInt64 itemId) + { + if (!IsInitialized) { return null; } + + var items = await GetWorkshopItems( + Steamworks.Ugc.Query.All + .WithFileId(itemId)); + return items.Any() ? items.First() : (Steamworks.Ugc.Item?)null; + } + + public static async Task ForceRedownload(UInt64 itemId) + => await ForceRedownload(new Steamworks.Ugc.Item(itemId)); + + public static void NukeDownload(Steamworks.Ugc.Item item) + { + try + { + System.IO.Directory.Delete(item.Directory, recursive: true); + } + catch + { + //don't care in the slightest about what happens here + } + } + + public static void Uninstall(Steamworks.Ugc.Item workshopItem) + { + NukeDownload(workshopItem); + var toUninstall + = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + .ToHashSet(); + toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); + } + + public static async Task ForceRedownload(Steamworks.Ugc.Item item) + { + NukeDownload(item); + await item.DownloadAsync(); + } + + /// + /// This class creates a file called ".copying" that + /// serves to keep mod copy operations in the same + /// directory from overlapping. + /// + private class CopyIndicator : IDisposable + { + private readonly string path; + + public CopyIndicator(string path) + { + this.path = path; + using (var f = File.Create(path)) + { + if (f is null) + { + throw new Exception($"File.Create returned null"); + } + f.WriteByte((byte)0); + } + } + + public void Dispose() + { + try + { + File.Delete(path); + } + catch + { + //don't care! + } + } + } + + /// + /// This class serves the purpose of preventing + /// more than 10 mod install tasks from proceeding + /// at the same time. + /// + private class InstallTaskCounter : IDisposable + { + private static readonly HashSet installers = new HashSet(); + private readonly static object mutex = new object(); + private const int MaxTasks = 7; + + private readonly UInt64 itemId; + private InstallTaskCounter(UInt64 id) { itemId = id; } + + public static bool IsInstalling(Steamworks.Ugc.Item item) + { + lock (mutex) + { + return installers.Any(i => i.itemId == item.Id); + } + } + + private async Task Init() + { + await Task.Yield(); + while (true) + { + lock (mutex) + { + if (installers.Count < MaxTasks) { installers.Add(this); return; } + } + await Task.Delay(5000); + } + } + + public static async Task Create(Steamworks.Ugc.Item item) + { + var retVal = new InstallTaskCounter(item.Id); + await retVal.Init(); + return retVal; + } + + public void Dispose() + { + lock (mutex) { installers.Remove(this); } + } + } + + public static bool IsItemDirectoryUpToDate(in Steamworks.Ugc.Item item) + { + string itemDirectory = item.Directory; + return Directory.Exists(itemDirectory) + && File.GetLastWriteTime(itemDirectory).ToUniversalTime() >= item.LatestUpdateTime; + } + + public static bool CanBeInstalled(ulong itemId) + => CanBeInstalled(new Steamworks.Ugc.Item(itemId)); + + public static bool CanBeInstalled(in Steamworks.Ugc.Item item) + { + bool needsUpdate = item.NeedsUpdate; + bool isDownloading = item.IsDownloading; + bool isInstalled = item.IsInstalled; + bool directoryIsUpToDate = IsItemDirectoryUpToDate(item); + + return !needsUpdate + && !isDownloading + && isInstalled + && directoryIsUpToDate; + } + + public static async Task DownloadModThenEnqueueInstall(Steamworks.Ugc.Item item) + { + if (!CanBeInstalled(item)) + { + if (!item.IsDownloading && !item.IsDownloadPending) { await ForceRedownload(item); } + } +#if CLIENT + else + { + OnItemDownloadComplete(item.Id); + } +#endif + } + + public static void DeleteFailedCopies() + { + foreach (var dir in Directory.EnumerateDirectories(ContentPackage.WorkshopModsDir, "**")) + { + string copyingIndicatorPath = Path.Combine(dir, ContentPackageManager.CopyIndicatorFileName); + if (File.Exists(copyingIndicatorPath)) + { + Directory.Delete(dir, recursive: true); + } + } + } + + public static bool IsInstallingToPath(string path) + => File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName)); + + public static bool IsInstalling(Steamworks.Ugc.Item item) + => InstallTaskCounter.IsInstalling(item); + + private static async Task InstallMod(ulong id) + { + var item = await GetItem(id); + if (item is null) { return; } + await InstallMod(item.Value); + } + + private static async Task InstallMod(Steamworks.Ugc.Item item) + { + await Task.Yield(); + using var installCounter = await InstallTaskCounter.Create(item); + + string itemTitle = item.Title.Trim(); + UInt64 itemId = item.Id; + string itemDirectory = item.Directory; + DateTime updateTime = item.LatestUpdateTime; + + if (!CanBeInstalled(item)) + { + ForceRedownload(item); + throw new InvalidOperationException($"Item {itemTitle} (id {itemId}) is not available for copying"); + } + + const string workshopModDirReadme = + "DO NOT MODIFY THE CONTENTS OF THIS FOLDER, EVEN IF\n" + + "YOU ARE EDITING A MOD YOU PUBLISHED YOURSELF.\n" + + "\n" + + "If you do you may run into networking issues and\n" + + "unexpected deletion of your hard work.\n" + + "Instead, modify a copy of your mod in LocalMods.\n"; + + string workshopModDirReadmeLocation = Path.Combine(SaveUtil.SaveFolder, "WorkshopMods", "README.txt"); + if (!File.Exists(workshopModDirReadmeLocation)) + { + Directory.CreateDirectory(Path.GetDirectoryName(workshopModDirReadmeLocation)!); + File.WriteAllText( + path: workshopModDirReadmeLocation, + contents: workshopModDirReadme); + } + + string installDir = Path.Combine(ContentPackage.WorkshopModsDir, itemId.ToString()); + Directory.CreateDirectory(installDir); + + string copyIndicatorPath = Path.Combine(installDir, ContentPackageManager.CopyIndicatorFileName); + + XDocument fileListSrc = XMLExtensions.TryLoadXml(Path.Combine(itemDirectory, ContentPackage.FileListFileName)); + string modName = fileListSrc.Root.GetAttributeString("name", item.Title).Trim(); + string modVersion = fileListSrc.Root.GetAttributeString("modversion", ContentPackage.DefaultModVersion); + Version gameVersion = fileListSrc.Root.GetAttributeVersion("gameversion", GameMain.Version); + bool isCorePackage = fileListSrc.Root.GetAttributeBool("corepackage", false); + string expectedHash = fileListSrc.Root.GetAttributeString("expectedhash", ""); + + using (var copyIndicator = new CopyIndicator(copyIndicatorPath)) + { + await CopyDirectory(itemDirectory, modName, itemDirectory, installDir); + + string fileListDestPath = Path.Combine(installDir, ContentPackage.FileListFileName); + XDocument fileListDest = XMLExtensions.TryLoadXml(fileListDestPath); + XElement root = fileListDest.Root ?? throw new NullReferenceException("Unable to install mod: file list root is null."); + root.Attributes().Remove(); + + root.Add( + new XAttribute("name", itemTitle), + new XAttribute("steamworkshopid", itemId), + new XAttribute("corepackage", isCorePackage), + new XAttribute("modversion", modVersion), + new XAttribute("gameversion", gameVersion), + new XAttribute("installtime", ToolBox.Epoch.FromDateTime(updateTime))); + if (modName.ToIdentifier() != itemTitle) + { + root.Add(new XAttribute("altnames", modName)); + } + if (!expectedHash.IsNullOrEmpty()) + { + root.Add(new XAttribute("expectedhash", expectedHash)); + } + fileListDest.SaveSafe(fileListDestPath); + } + } + + private static async Task CorrectPaths(string fileListDir, string modName, XElement element) + { + foreach (var attribute in element.Attributes()) + { + await Task.Yield(); + + string val = attribute.Value.CleanUpPathCrossPlatform(correctFilenameCase: false); + + //Handle really old mods (0.9.0.4-era) that might be structured as + //%ModDir%/Mods/[NAME]/[RESOURCE] + string fullSrcPath = Path.Combine(fileListDir, val).CleanUpPath(); + if (File.Exists(fullSrcPath)) + { + val = $"{ContentPath.ModDirStr}/{val}"; + } + + //Handle old mods that installed to the fixed Mods directory + //that no longer exists + string oldModDir = $"Mods/{modName}"; + if (val.StartsWith(oldModDir, StringComparison.OrdinalIgnoreCase)) + { + val = $"{ContentPath.ModDirStr}{val.Remove(0, oldModDir.Length)}"; + } + //Handle old mods that depend on other mods + else if (val.StartsWith("Mods/", StringComparison.OrdinalIgnoreCase)) + { + string otherModName = val.Substring(val.IndexOf('/')+1); + otherModName = otherModName.Substring(0, otherModName.IndexOf('/')); + val = $"{string.Format(ContentPath.OtherModDirFmt, otherModName)}{val.Remove(0, $"Mods/{otherModName}".Length)}"; + } + //Handle really old mods that installed Submarines in the wrong place + else if (val.StartsWith("Submarines/", StringComparison.OrdinalIgnoreCase)) + { + val = $"{ContentPath.ModDirStr}/{val}"; + } + attribute.Value = val; + } + await Task.WhenAll( + element.Elements() + .Select(subElement => CorrectPaths( + fileListDir: fileListDir, + modName: modName, + element: subElement))); + } + + private static async Task CopyFile(string fileListDir, string modName, string from, string to) + { + await Task.Yield(); + Identifier extension = Path.GetExtension(from).ToIdentifier(); + if (extension == ".xml") + { + try + { + XDocument? doc = XMLExtensions.TryLoadXml(from, out var exception); + if (exception is { Message: string exceptionMsg }) + { + throw new Exception($"Could not load \"{from}\": {exceptionMsg}"); + } + if (doc is null) + { + throw new Exception($"Could not load \"{from}\": doc is null"); + } + await CorrectPaths( + fileListDir: fileListDir, + modName: modName, + element: doc.Root ?? throw new NullReferenceException()); + doc.SaveSafe(to); + return; + } + catch (Exception e) + { + DebugConsole.ThrowError( + $"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}"); + } + } + File.Copy(from, to, overwrite: true); + } + + private static async Task CopyDirectory(string fileListDir, string modName, string from, string to) + { + from = Path.GetFullPath(from); to = Path.GetFullPath(to); + Directory.CreateDirectory(to); + + string convertFromTo(string from) + => Path.Combine(to, Path.GetFileName(from)); + + string[] files = Directory.GetFiles(from); + string[] subDirs = Directory.GetDirectories(from); + foreach (var file in files) + { + await CopyFile(fileListDir, modName, file, convertFromTo(file)); + } + + foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir)); } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index c6b96185c..4a5731b4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -14,7 +14,7 @@ namespace Barotrauma { private const float UpdateInterval = 1.0f; - private static HashSet unlockedAchievements = new HashSet(); + private static HashSet unlockedAchievements = new HashSet(); public static bool CheatsEnabled = false; @@ -68,7 +68,7 @@ namespace Barotrauma { if (GameMain.GameSession.EventManager.CurrentIntensity > 0.99f) { - UnlockAchievement("maxintensity", true, c => c != null && !c.IsDead && !c.IsUnconscious); + UnlockAchievement("maxintensity".ToIdentifier(), true, c => c != null && !c.IsDead && !c.IsUnconscious); } foreach (Character c in Character.CharacterList) @@ -84,7 +84,7 @@ namespace Barotrauma else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth * 0.5f) { //all characters that have entered crush depth and are still alive get an achievement - if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth"); + if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); } } } @@ -110,7 +110,7 @@ namespace Barotrauma if (Math.Abs(submarineVel.X) > 100.0f) { //all conscious characters inside the sub get an achievement - UnlockAchievement("subhighvelocity", true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); + UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); } //achievement for descending ridiculously deep @@ -118,7 +118,7 @@ namespace Barotrauma if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { //all conscious characters inside the sub get an achievement - UnlockAchievement("subdeep", true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); + UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); } } @@ -151,13 +151,13 @@ namespace Barotrauma { if (c == null || c.Removed) { return; } - if (c.HasEquippedItem("clownmask") && - c.HasEquippedItem("clowncostume")) + if (c.HasEquippedItem("clownmask".ToIdentifier()) && + c.HasEquippedItem("clowncostume".ToIdentifier())) { - UnlockAchievement(c, "clowncostume"); + UnlockAchievement(c, "clowncostume".ToIdentifier()); } - if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName == CharacterPrefab.HumanSpeciesName) { float requiredDist = 500 / Physics.DisplayToRealWorldRatio; float distSquared = Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition); @@ -187,7 +187,7 @@ namespace Barotrauma } if (distSquared > requiredDist * requiredDist) { - UnlockAchievement(c, "crewaway"); + UnlockAchievement(c, "crewaway".ToIdentifier()); } static CachedDistance CalculateNewCachedDistance(Character c) @@ -218,7 +218,7 @@ namespace Barotrauma public static void OnBiomeDiscovered(Biome biome) { - UnlockAchievement("discover" + biome.Identifier.ToLowerInvariant().Replace(" ", "")); + UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); } public static void OnItemRepaired(Item item, Character fixer) @@ -228,13 +228,13 @@ namespace Barotrauma #endif if (fixer == null) { return; } - UnlockAchievement(fixer, "repairdevice"); - UnlockAchievement(fixer, "repair" + item.Prefab.Identifier); + UnlockAchievement(fixer, "repairdevice".ToIdentifier()); + UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier()); } public static void OnAfflictionRemoved(Affliction affliction, Character character) { - if (string.IsNullOrEmpty(affliction.Prefab.AchievementOnRemoved)) { return; } + if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; } #if CLIENT if (GameMain.Client != null) { return; } @@ -248,7 +248,7 @@ namespace Barotrauma if (GameMain.Client != null) { return; } #endif if (reviver == null) { return; } - UnlockAchievement(reviver, "healcrit"); + UnlockAchievement(reviver, "healcrit".ToIdentifier()); } public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath) @@ -260,68 +260,67 @@ namespace Barotrauma causeOfDeath.Killer != null && causeOfDeath.Killer == Character.Controlled) { - IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); + IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1); } #elif SERVER if (character != causeOfDeath.Killer && causeOfDeath.Killer != null) { - IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); + IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1); } #endif roundData?.Casualties.Add(character); - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier()); if (character.CurrentHull != null) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName + "indoors"); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}indoors".ToIdentifier()); } if (character.SpeciesName.EndsWith("boss")) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("boss", "")); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier()); if (character.CurrentHull != null) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("boss", "") + "indoors"); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier()); } } if (character.SpeciesName.EndsWith("_m")) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("_m", "")); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier()); if (character.CurrentHull != null) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("_m", "") + "indoors"); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier()); } } - if (character.HasEquippedItem("clownmask") && - character.HasEquippedItem("clowncostume") && + if (character.HasEquippedItem("clownmask".ToIdentifier()) && + character.HasEquippedItem("clowncostume".ToIdentifier()) && causeOfDeath.Killer != character) { - UnlockAchievement(causeOfDeath.Killer, "killclown"); + UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier()); } if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null) { - UnlockAchievement(causeOfDeath.Killer, "killpoison"); + UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); } if (causeOfDeath.DamageSource is Item item) { if (item.HasTag("tool")) { - UnlockAchievement(causeOfDeath.Killer, "killtool"); + UnlockAchievement(causeOfDeath.Killer, "killtool".ToIdentifier()); } else { - switch (item.Prefab.Identifier) + if (item.Prefab.Identifier == "morbusine") { - case "morbusine": - UnlockAchievement(causeOfDeath.Killer, "killpoison"); - break; - case "nuclearshell": - case "nucleardepthcharge": - UnlockAchievement(causeOfDeath.Killer, "killnuke"); - break; + UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); + } + else if (item.Prefab.Identifier == "nuclearshell" || + item.Prefab.Identifier == "nucleardepthcharge") + { + UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier()); } } } @@ -331,7 +330,7 @@ namespace Barotrauma { if (GameMain.Server.TraitorManager.IsTraitor(character)) { - UnlockAchievement(causeOfDeath.Killer, "killtraitor"); + UnlockAchievement(causeOfDeath.Killer, "killtraitor".ToIdentifier()); } } #endif @@ -342,7 +341,7 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null || GameMain.GameSession == null) { return; } #endif - UnlockAchievement(character, "traitorwin"); + UnlockAchievement(character, "traitorwin".ToIdentifier()); } public static void OnRoundEnded(GameSession gameSession) @@ -363,14 +362,14 @@ namespace Barotrauma !myCharacter.IsDead && (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost))) { - IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers); } #endif } else { //in sp making it to the end is enough - IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers); } } @@ -384,7 +383,10 @@ namespace Barotrauma if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) { //all characters that are alive and in the winning team get an achievement - UnlockAchievement(mission.Prefab.AchievementIdentifier + (int)GameMain.GameSession.WinningTeam, true, + var achvIdentifier = + $"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}" + .ToIdentifier(); + UnlockAchievement(achvIdentifier, true, c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c)); } else if (mission.Completed) @@ -410,18 +412,18 @@ namespace Barotrauma if (GameMain.Server != null) { //in MP all characters that were inside the sub during reactor meltdown and still alive at the end of the round get an achievement - UnlockAchievement("survivereactormeltdown", true, c => c != null && !c.IsDead && roundData.ReactorMeltdown.Contains(c)); + UnlockAchievement("survivereactormeltdown".ToIdentifier(), true, c => c != null && !c.IsDead && roundData.ReactorMeltdown.Contains(c)); if (noDamageRun) { - UnlockAchievement("nodamagerun", true, c => c != null && !c.IsDead); + UnlockAchievement("nodamagerun".ToIdentifier(), true, c => c != null && !c.IsDead); } } #endif #if CLIENT - if (noDamageRun) { UnlockAchievement("nodamagerun"); } + if (noDamageRun) { UnlockAchievement("nodamagerun".ToIdentifier()); } if (roundData.ReactorMeltdown.Any()) //in SP getting to the destination after a meltdown is enough { - UnlockAchievement("survivereactormeltdown"); + UnlockAchievement("survivereactormeltdown".ToIdentifier()); } #endif var charactersInSub = Character.CharacterList.FindAll(c => @@ -435,12 +437,12 @@ namespace Barotrauma //there must be some non-enemy casualties to get the last mant standing achievement if (roundData.Casualties.Any(c => !(c.AIController is EnemyAIController) && c.TeamID == charactersInSub[0].TeamID)) { - UnlockAchievement(charactersInSub[0], "lastmanstanding"); + UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier()); } #if CLIENT else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1) { - UnlockAchievement(charactersInSub[0], "lonesailor"); + UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier()); } #else //lone sailor achievement if alone in the sub and there are no other characters with the same team ID @@ -449,7 +451,7 @@ namespace Barotrauma c.TeamID == charactersInSub[0].TeamID && !(c.AIController is EnemyAIController))) { - UnlockAchievement(charactersInSub[0], "lonesailor"); + UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier()); } #endif @@ -457,14 +459,14 @@ namespace Barotrauma foreach (Character character in charactersInSub) { if (character.Info.Job == null) { continue; } - UnlockAchievement(character, character.Info.Job.Prefab.Identifier + "round"); + UnlockAchievement(character, $"{character.Info.Job.Prefab.Identifier}round".ToIdentifier()); } } pathFinder = null; } - private static void UnlockAchievement(Character recipient, string identifier) + private static void UnlockAchievement(Character recipient, Identifier identifier) { if (CheatsEnabled || recipient == null) { return; } #if CLIENT @@ -477,7 +479,7 @@ namespace Barotrauma #endif } - private static void IncrementStat(Character recipient, string identifier, int amount) + private static void IncrementStat(Character recipient, Identifier identifier, int amount) { if (CheatsEnabled || recipient == null) { return; } #if CLIENT @@ -490,23 +492,22 @@ namespace Barotrauma #endif } - public static void IncrementStat(string identifier, int amount) + public static void IncrementStat(Identifier identifier, int amount) { if (CheatsEnabled) { return; } SteamManager.IncrementStat(identifier, amount); } - public static void IncrementStat(string identifier, float amount) + public static void IncrementStat(Identifier identifier, float amount) { if (CheatsEnabled) { return; } SteamManager.IncrementStat(identifier, amount); } - public static void UnlockAchievement(string identifier, bool unlockClients = false, Func conditions = null) + public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func conditions = null) { if (CheatsEnabled) { return; } - identifier = identifier.ToLowerInvariant(); - + #if SERVER if (unlockClients && GameMain.Server != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/AddedPunctuationLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/AddedPunctuationLString.cs new file mode 100644 index 000000000..21ba71b98 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/AddedPunctuationLString.cs @@ -0,0 +1,35 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class AddedPunctuationLString : LocalizedString + { + private readonly ImmutableArray nestedStrs; + private readonly char punctuationSymbol; + + public AddedPunctuationLString(char symbol, params LocalizedString[] nStrs) { nestedStrs = nStrs.ToImmutableArray(); punctuationSymbol = symbol; } + + public override bool Loaded => nestedStrs.All(s => s.Loaded); + public override void RetrieveValue() + { + string separator = ""; + if (GameSettings.CurrentConfig.Language == "French".ToLanguageIdentifier()) + { + bool addNonBreakingSpace = + punctuationSymbol == ':' || punctuationSymbol == ';' || + punctuationSymbol == '!' || punctuationSymbol == '?'; + separator = addNonBreakingSpace ? + new string(new char[] { (char)(0xA0), punctuationSymbol, ' ' }) : + new string(new char[] { punctuationSymbol, ' ' }); + } + else + { + separator = new string(new char[] { punctuationSymbol, ' ' }); + } + cachedValue = string.Join(separator, nestedStrs.Select(str => str.Value)); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/CapitalizeLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/CapitalizeLString.cs new file mode 100644 index 000000000..a056f31dd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/CapitalizeLString.cs @@ -0,0 +1,25 @@ +#nullable enable +namespace Barotrauma +{ + public class CapitalizeLString : LocalizedString + { + private readonly LocalizedString nestedStr; + + public CapitalizeLString(LocalizedString nStr) { nestedStr = nStr; } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + string str = nestedStr.Value; + if (!string.IsNullOrEmpty(str)) + { + cachedValue = char.ToUpper(str[0]) + str[1..]; + } + else + { + cachedValue = ""; + } + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs new file mode 100644 index 000000000..8048609fc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace Barotrauma +{ + public class ConcatLString : LocalizedString + { + private readonly LocalizedString left; + private readonly LocalizedString right; + + public ConcatLString(LocalizedString l, LocalizedString r) + { + left = l; right = r; + } + + public override bool Loaded => left.Loaded || right.Loaded; + public override void RetrieveValue() + { + cachedValue = left.Value + right.Value; + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs new file mode 100644 index 000000000..d4ca38f08 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs @@ -0,0 +1,44 @@ +#nullable enable +namespace Barotrauma +{ + public class FallbackLString : LocalizedString + { + private readonly LocalizedString primary; + private readonly LocalizedString fallback; + + private bool primaryIsLoaded = false; + + public FallbackLString(LocalizedString primary, LocalizedString fallback) + { + if (primary is FallbackLString {primary: { } innerPrimary, fallback: { } innerFallback}) + { + this.primary = innerPrimary; + this.fallback = innerFallback.Fallback(fallback); + } + else + { + this.primary = primary; + this.fallback = fallback; + } + } + + protected override bool MustRetrieveValue() + { + return base.MustRetrieveValue() + || MustRetrieveValue(primary) + || MustRetrieveValue(fallback) + || primaryIsLoaded != primary.Loaded; + } + + public override bool Loaded => primary.Loaded || fallback.Loaded; + public override void RetrieveValue() + { + cachedValue = primary.Value; + primaryIsLoaded = primary.Loaded; + if (!primary.Loaded) + { + cachedValue = fallback.Value; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FormattedLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FormattedLString.cs new file mode 100644 index 000000000..7e652ecec --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FormattedLString.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class FormattedLString : LocalizedString + { + private readonly LocalizedString str; + private readonly ImmutableArray subStrs; + public FormattedLString(LocalizedString str, params LocalizedString[] subStrs) + { + this.str = str; + this.subStrs = subStrs.ToImmutableArray(); + } + + public override bool Loaded => str.Loaded && subStrs.All(s => s.Loaded); + public override void RetrieveValue() + { + //TODO: possibly broken! + cachedValue = string.Format(str.Value, subStrs.Select(s => s.Value as object).ToArray()); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs new file mode 100644 index 000000000..cfd7c255b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs @@ -0,0 +1,33 @@ +#nullable enable +using System; + +namespace Barotrauma +{ + public class InputTypeLString : LocalizedString + { + private readonly LocalizedString nestedStr; + public InputTypeLString(LocalizedString nStr) { nestedStr = nStr; } + + protected override bool MustRetrieveValue() + { + //TODO: check for config changes! + return base.MustRetrieveValue(); + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value; +#if CLIENT + //TODO: server shouldn't have this type at all + foreach (InputType? inputType in Enum.GetValues(typeof(InputType))) + { + if (!inputType.HasValue) { continue; } + cachedValue = cachedValue.Replace($"[{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase); + cachedValue = cachedValue.Replace($"[InputType.{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase); + } +#endif + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/JoinLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/JoinLString.cs new file mode 100644 index 000000000..9571bdb2f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/JoinLString.cs @@ -0,0 +1,24 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + public class JoinLString : LocalizedString + { + private readonly IEnumerable subStrs; + private readonly string separator; + + public JoinLString(string separator, IEnumerable subStrs) + { + this.separator = separator; this.subStrs = subStrs; + } + + public override bool Loaded => subStrs.All(s => s.Loaded); + public override void RetrieveValue() + { + cachedValue = string.Join(separator, subStrs); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs new file mode 100644 index 000000000..7f27612ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs @@ -0,0 +1,177 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + public abstract class LocalizedString : IComparable + { + protected enum LoadedSuccessfully + { + Unknown, + No, + Yes + } + + protected LanguageIdentifier language { get; private set; } = LanguageIdentifier.None; + private int languageVersion = 0; + + protected string cachedValue = ""; + public string Value + { + get + { + if (MustRetrieveValue()) { RetrieveValue(); } + return cachedValue; + } + } + + public int Length => Value.Length; + + public abstract bool Loaded { get; } + + protected void UpdateLanguage() + { + language = GameSettings.CurrentConfig.Language; + languageVersion = TextManager.LanguageVersion; + } + + protected virtual bool MustRetrieveValue() //this can't be called on other LocalizedStrings by derived classes + { + return language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; + } + + protected static bool MustRetrieveValue(LocalizedString str) //this can be called by derived classes + { + return str.MustRetrieveValue(); + } + + public abstract void RetrieveValue(); + + public static implicit operator LocalizedString(string value) => new RawLString(value); + public static implicit operator LocalizedString(char value) => new RawLString(value.ToString()); + + public static LocalizedString operator+(LocalizedString left, LocalizedString right) => new ConcatLString(left, right); + public static LocalizedString operator+(LocalizedString left, object right) => left + (right.ToString() ?? ""); + public static LocalizedString operator+(object left, LocalizedString right) => (left.ToString() ?? "") + right; + + public static bool operator==(LocalizedString? left, LocalizedString? right) + { + return left?.Value == right?.Value; + } + + public static bool operator!=(LocalizedString? left, LocalizedString? right) + { + return !(left == right); + } + + public override string ToString() + { + return Value; + } + + public bool Contains(string subStr, StringComparison comparison = StringComparison.Ordinal) + { + return !Value.IsNullOrEmpty() && Value.Contains(subStr, comparison); + } + + public bool Contains(char chr, StringComparison comparison = StringComparison.Ordinal) + { + return Value.Contains(chr, comparison); + } + + public virtual LocalizedString ToUpper() + { + return new UpperLString(this); + } + + public static LocalizedString Join(string separator, params LocalizedString[] subStrs) + { + return Join(separator, (IEnumerable)subStrs); + } + + public static LocalizedString Join(string separator, IEnumerable subStrs) + { + return new JoinLString(separator, subStrs); + } + + public LocalizedString Fallback(LocalizedString fallback) + { + return new FallbackLString(this, fallback); + } + + public IReadOnlyList Split(params char[] separators) + { + var splitter = new LStringSplitter(this, separators); + return splitter.Substrings; + } + + public LocalizedString Replace(Identifier find, LocalizedString replace, StringComparison stringComparison = StringComparison.Ordinal) + { + return new ReplaceLString(this, stringComparison, (find, replace)); + } + + public LocalizedString Replace(string find, LocalizedString replace, StringComparison stringComparison = StringComparison.Ordinal) + { + return new ReplaceLString(this, stringComparison, (find.ToIdentifier(), replace)); + } + + public LocalizedString Replace(LocalizedString find, LocalizedString replace, + StringComparison stringComparison = StringComparison.Ordinal) + { + return new ReplaceLString(this, stringComparison, (find, replace)); + } + + public LocalizedString TrimStart() + { + return new TrimLString(this, TrimLString.Mode.Start); + } + + public LocalizedString TrimEnd() + { + return new TrimLString(this, TrimLString.Mode.End); + } + + public LocalizedString ToLower() + { + return new LowerLString(this); + } + + public override bool Equals(object? obj) + { + if (obj is LocalizedString lStr) { return Equals(lStr, StringComparison.Ordinal); } + if (obj is string str) { return Equals(str, StringComparison.Ordinal); } + return base.Equals(obj); + } + + public bool Equals(LocalizedString other, StringComparison comparison = StringComparison.Ordinal) + { + return Equals(other.Value, comparison); + } + + public bool Equals(string other, StringComparison comparison = StringComparison.Ordinal) + { + return string.Equals(Value, other, comparison); + } + + public bool StartsWith(LocalizedString other, StringComparison comparison = StringComparison.Ordinal) + { + return StartsWith(other.Value, comparison); + } + + public bool StartsWith(string other, StringComparison comparison = StringComparison.Ordinal) + { + return Value.StartsWith(other, comparison); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public int CompareTo(object? obj) + { + return Value.CompareTo(obj?.ToString() ?? ""); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LowerLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LowerLString.cs new file mode 100644 index 000000000..7175919f9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LowerLString.cs @@ -0,0 +1,20 @@ +#nullable enable +namespace Barotrauma +{ + public class LowerLString : LocalizedString + { + private readonly LocalizedString nestedStr; + + public LowerLString(LocalizedString nestedStr) + { + this.nestedStr = nestedStr; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value.ToLowerInvariant(); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/RawLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/RawLString.cs new file mode 100644 index 000000000..75fd64394 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/RawLString.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace Barotrauma +{ + public class RawLString : LocalizedString + { + public RawLString(string value) { cachedValue = value; } + + protected override bool MustRetrieveValue() => false; + + public override bool Loaded => true; + public override void RetrieveValue() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ReplaceLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ReplaceLString.cs new file mode 100644 index 000000000..ceced3a14 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ReplaceLString.cs @@ -0,0 +1,75 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + public class ReplaceLString : LocalizedString + { + private readonly LocalizedString nestedStr; + private readonly ImmutableDictionary replacements; + private readonly StringComparison stringComparison; + + public ReplaceLString(LocalizedString nStr, StringComparison sc, IEnumerable<(LocalizedString Key, LocalizedString Value, FormatCapitals FormatCapitals)> r) + { + nestedStr = nStr; + replacements = r.Select(kvf => (kvf.Key, (kvf.Value, kvf.FormatCapitals))).ToImmutableDictionary(); + stringComparison = sc; + } + + public ReplaceLString(LocalizedString nStr, StringComparison sc, params (LocalizedString Key, LocalizedString Value)[] r) + : this(nStr, sc, r.Select(kv => (kv.Key, kv.Value, FormatCapitals.No))) { } + + public ReplaceLString(LocalizedString nStr, StringComparison sc, IEnumerable<(Identifier Key, LocalizedString Value, FormatCapitals FormatCapitals)> r) + : this(nStr, sc, r.Select(p => ((LocalizedString)p.Key.Value, p.Value, p.FormatCapitals))) { } + + public ReplaceLString(LocalizedString nStr, StringComparison sc, params (Identifier Key, LocalizedString Value)[] r) + : this(nStr, sc, r.Select(kv => ((LocalizedString)kv.Key.Value, kv.Value, FormatCapitals.No))) { } + + private static string HandleVariableCapitalization(string text, string variableTag, string variableValue) + { + int index = text.IndexOf(variableTag, StringComparison.InvariantCulture) - 1; + if (index == -1) + { + return variableValue; + } + + for (int i = index; i >= 0; i--) + { + if (char.IsWhiteSpace(text[i])) { continue; } + + if (text[i] != '.') + { + variableValue = variableValue.ToLowerInvariant(); + } + else + { + variableValue = TextManager.Capitalize(variableValue).Value; + break; + } + } + + return variableValue; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value; + foreach (var varName in replacements.Keys) + { + string key = varName.Value; + string value = replacements[varName].Value.Value; + if (replacements[varName].FormatCapitals == FormatCapitals.Yes) + { + value = HandleVariableCapitalization(cachedValue, key, value); + } + cachedValue = cachedValue.Replace(key, value, stringComparison); + } + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ServerMsgLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ServerMsgLString.cs new file mode 100644 index 000000000..c95b4f3bb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ServerMsgLString.cs @@ -0,0 +1,185 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Barotrauma +{ + public class ServerMsgLString : LocalizedString + { + private static readonly Regex reFormattedMessage = + new Regex(@"^(?[\[\].a-z0-9_]+?)=(?[a-z0-9_]+?)\((?.+?)\)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex reReplacedMessage = new Regex(@"^(?[\[\].a-z0-9_]+?)=(?.*)$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly ImmutableDictionary> messageFormatters = + new Dictionary> + { + { + "duration".ToIdentifier(), + secondsValue => double.TryParse(secondsValue, out var seconds) + ? $"{TimeSpan.FromSeconds(seconds):g}" + : null + } + }.ToImmutableDictionary(); + + private static readonly ImmutableHashSet serverMessageCharacters = + new [] {'~', '[', ']', '='}.ToImmutableHashSet(); + + private readonly string serverMessage; + private readonly ImmutableArray messageSplit; + + public ServerMsgLString(string serverMsg) + { + serverMessage = serverMsg; + messageSplit = serverMessage.Split("/").ToImmutableArray(); + } + + private static bool IsServerMessageWithVariables(string message) => + serverMessageCharacters.All(message.Contains); + + private LoadedSuccessfully loadedSuccessfully = LoadedSuccessfully.Unknown; + public override bool Loaded + { + get + { + if (loadedSuccessfully == LoadedSuccessfully.Unknown) { RetrieveValue(); } + return loadedSuccessfully == LoadedSuccessfully.Yes; + } + } + + public override void RetrieveValue() + { + Dictionary replacedMessages = new Dictionary(); + bool translationsFound = false; + + string? TranslateMessage(string input) + { + string? message = input; + if (message.EndsWith("~", StringComparison.Ordinal)) + { + message = message.Substring(0, message.Length - 1); + } + + if (!IsServerMessageWithVariables(message) && !message.Contains('=')) // No variables, try to translate + { + foreach (var replacedMessage in replacedMessages) + { + message = message.Replace(replacedMessage.Key, replacedMessage.Value); + } + + if (message.Contains(" ")) + { + return message; + } // Spaces found, do not translate + + var msg = TextManager.Get(message); + if (msg.Loaded) // If a translation was found, otherwise use the original + { + message = msg.Value; + translationsFound = true; + } + } + else + { + string? messageVariable = null; + var matchFormatted = reFormattedMessage.Match(message); + if (matchFormatted.Success) + { + var formatter = matchFormatted.Groups["formatter"].ToString().ToIdentifier(); + if (messageFormatters.TryGetValue(formatter, out var formatterFn)) + { + var formattedValue = formatterFn(matchFormatted.Groups["value"].ToString()); + if (formattedValue != null) + { + messageVariable = matchFormatted.Groups["variable"].ToString(); + message = formattedValue; + } + } + } + + if (messageVariable == null) + { + var matchReplaced = reReplacedMessage.Match(message); + if (matchReplaced.Success) + { + messageVariable = matchReplaced.Groups["variable"].ToString(); + message = matchReplaced.Groups["message"].ToString(); + } + } + + foreach (var replacedMessage in replacedMessages) + { + message = message.Replace(replacedMessage.Key, replacedMessage.Value); + } + + string[] messageWithVariables = message.Split('~'); + + var msg = TextManager.Get(messageWithVariables[0]); + + if (msg.Loaded) // If a translation was found, otherwise use the original + { + message = msg.Value; + translationsFound = true; + } + else if (messageVariable == null) + { + return message; // No translation found, probably caused by player input -> skip variable handling + } + + // First index is always the message identifier -> start at 1 + for (int j = 1; j < messageWithVariables.Length; j++) + { + string[] variableAndValue = messageWithVariables[j].Split('='); + message = message.Replace(variableAndValue[0], + variableAndValue[1].Length > 1 && variableAndValue[1][0] == '§' + ? TextManager.Get(variableAndValue[1].Substring(1)).Value + : variableAndValue[1]); + } + + if (messageVariable != null) + { + replacedMessages[messageVariable] = message; + message = null; + } + } + + return message; + } + + try + { + string translatedServerMessage = ""; + + for (int i = 0; i < messageSplit.Length; i++) + { + string? message = TranslateMessage(messageSplit[i]); + if (message != null) + { + translatedServerMessage += message; + } + } + + cachedValue = translationsFound ? translatedServerMessage : serverMessage; + loadedSuccessfully = LoadedSuccessfully.Yes; + } + catch (IndexOutOfRangeException exception) + { + string errorMsg = $"Failed to translate server message \"{serverMessage}\"."; +#if DEBUG + DebugConsole.ThrowError(errorMsg, exception); +#endif + GameAnalyticsManager.AddErrorEventOnce($"TextManager.GetServerMessage:{serverMessage}", + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + cachedValue = errorMsg; + loadedSuccessfully = LoadedSuccessfully.No; + } + + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/SplitLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/SplitLString.cs new file mode 100644 index 000000000..d30ae2a00 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/SplitLString.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +#nullable enable +namespace Barotrauma +{ + public class LStringSplitter + { + public IReadOnlyList Substrings => substrings; + + private class SubstringList : IReadOnlyList + { + public SubstringList(LStringSplitter splitter) { this.splitter = splitter; } + + private LStringSplitter splitter; + private readonly List underlyingList = new List(); + + public List UnderlyingList + { + get + { + splitter.UpdateSubstrings(); + return underlyingList; + } + } + + public IEnumerator GetEnumerator() => UnderlyingList.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => UnderlyingList.Count; + + public LocalizedString this[int index] => UnderlyingList[index]; + } + + private readonly SubstringList substrings; + private readonly char[] separators; + private readonly LocalizedString originalString; + private string[] substrValues; + + private string cachedOriginal; + + public bool Loaded => originalString.Loaded; + + public LStringSplitter(LocalizedString input, params char[] separators) + { + originalString = input; + substrings = new SubstringList(this); + substrValues = Array.Empty(); + this.separators = separators; + cachedOriginal = ""; + } + + private void UpdateSubstrings() + { + if (originalString.Value != cachedOriginal) + { + cachedOriginal = originalString.Value; + substrValues = cachedOriginal.Split(separators); + substrings.UnderlyingList.Clear(); + substrings.UnderlyingList.AddRange(Enumerable.Range(0, substrValues.Length).Select(i => new SplitLString(this, i) as LocalizedString)); + } + } + + public string GetValue(int index) + { + UpdateSubstrings(); + return substrValues[index]; + } + } + + public class SplitLString : LocalizedString + { + private bool loaded = false; + private readonly LStringSplitter splitter; + private readonly int index; + + public SplitLString(LStringSplitter splitter, int index) + { + this.splitter = splitter; this.index = index; + } + + public override bool Loaded => loaded && splitter.Loaded; + public override void RetrieveValue() + { + loaded = true; + cachedValue = splitter.GetValue(index); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs new file mode 100644 index 000000000..0056fc9da --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -0,0 +1,71 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + public class TagLString : LocalizedString + { + private readonly ImmutableArray tags; + + public TagLString(params Identifier[] tags) + { + this.tags = tags.ToImmutableArray(); + } + + private LoadedSuccessfully loadedSuccessfully = LoadedSuccessfully.Unknown; + + public override bool Loaded + { + get + { + if (loadedSuccessfully == LoadedSuccessfully.Unknown) { RetrieveValue(); } + return loadedSuccessfully == LoadedSuccessfully.Yes; + } + } + + public override void RetrieveValue() + { + UpdateLanguage(); + + (string value, bool loaded) tryLoad(LanguageIdentifier lang) + { + IReadOnlyList candidates = Array.Empty(); + int tagIndex = 0; + + if (TextManager.TextPacks.TryGetValue(lang, out var packs)) + { + while (candidates.Count == 0 && tagIndex < tags.Length) + { + foreach (var pack in packs) + { + if (pack.Texts.TryGetValue(tags[tagIndex], out var texts)) + { + candidates = candidates.ListConcat(texts); + } + } + tagIndex++; + } + } + + bool loaded = candidates.Count > 0; + return (loaded ? candidates.GetRandomUnsynced() : "", loaded); + } + + var (value, loaded) = tryLoad(language); + loadedSuccessfully = loaded ? LoadedSuccessfully.Yes : LoadedSuccessfully.No; + cachedValue = value; + if (!loaded && language != TextManager.DefaultLanguage) + { + (value, _) = tryLoad(TextManager.DefaultLanguage); + cachedValue = value; + //Notice how we don't set loadedSuccessfully again here. + //This is by design; falling back to English means that + //this text did NOT load successfully, so Loaded must + //return false. + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs new file mode 100644 index 000000000..92c987b9e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs @@ -0,0 +1,28 @@ +#nullable enable +using System; + +namespace Barotrauma +{ + public class TrimLString : LocalizedString + { + [Flags] + public enum Mode { Start = 0x1, End = 0x2, Both=0x3 } + private readonly LocalizedString nestedStr; + private readonly Mode mode; + + public TrimLString(LocalizedString nestedStr, Mode mode) + { + this.nestedStr = nestedStr; + this.mode = mode; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value; + if (mode.HasFlag(Mode.Start)) { cachedValue = cachedValue.TrimStart(); } + if (mode.HasFlag(Mode.End)) { cachedValue = cachedValue.TrimEnd(); } + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/UpperLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/UpperLString.cs new file mode 100644 index 000000000..8c75fb7f0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/UpperLString.cs @@ -0,0 +1,25 @@ +#nullable enable +namespace Barotrauma +{ + public class UpperLString : LocalizedString + { + private readonly LocalizedString nestedStr; + + public UpperLString(LocalizedString nestedStr) + { + this.nestedStr = nestedStr; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value.ToUpper(); + UpdateLanguage(); + } + + public override LocalizedString ToUpper() + { + return this; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs new file mode 100644 index 000000000..30e3678e7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -0,0 +1,199 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Barotrauma +{ + public class RichString + { + protected bool loaded = false; + protected LanguageIdentifier language = LanguageIdentifier.None; + + protected string cachedSanitizedValue = ""; + public string SanitizedValue + { + get + { + if (MustRetrieveValue()) { RetrieveValue(); } + return cachedSanitizedValue; + } + } + + public int Length => SanitizedValue.Length; + + private readonly Func? postProcess; + private readonly bool shouldParseRichTextData; + + private readonly LocalizedString originalStr; + public LocalizedString NestedStr { get; private set; } + public readonly LocalizedString SanitizedString; + +#if CLIENT + private readonly GUIFont? font; + private readonly GUIComponentStyle? componentStyle; + private bool forceUpperCase = false; + + private bool fontOrStyleForceUpperCase + => font is { ForceUpperCase: true } || componentStyle is { ForceUpperCase: true }; +#endif + + public ImmutableArray? RichTextData { get; private set; } + +#if CLIENT + private RichString( + LocalizedString nestedStr, bool shouldParseRichTextData, Func? postProcess = null, + GUIFont? font = null, GUIComponentStyle? componentStyle = null) : this(nestedStr, shouldParseRichTextData, postProcess) + { + this.font = font; + this.componentStyle = componentStyle; + } +#endif + + private RichString(LocalizedString nestedStr, bool shouldParseRichTextData, Func? postProcess = null) + { + originalStr = nestedStr; + NestedStr = originalStr; + this.shouldParseRichTextData = shouldParseRichTextData; + this.postProcess = postProcess; + SanitizedString = new StripRichTagsLString(this); +#if CLIENT + this.font = null; + this.componentStyle = null; +#endif + } + + public static RichString Rich(LocalizedString str, Func? postProcess = null) + { + return new RichString(str, true, postProcess); + } + + public static RichString Plain(LocalizedString str) + { + return new RichString(str, false, postProcess: null); + } + + public static implicit operator LocalizedString(RichString richStr) => richStr.NestedStr; + + public static implicit operator RichString(LocalizedString lStr) + { +#if DEBUG + if (!lStr.IsNullOrEmpty() && lStr.Contains("‖")) + { + if (Debugger.IsAttached) { Debugger.Break(); } + } +#endif + return Plain(lStr); + } + public static implicit operator RichString(string str) => (LocalizedString)str; + + protected virtual bool MustRetrieveValue() + { + return NestedStr.Loaded != loaded + || language != GameSettings.CurrentConfig.Language +#if CLIENT + || (fontOrStyleForceUpperCase != forceUpperCase) +#endif + ; + } + + public void RetrieveValue() + { +#if CLIENT + NestedStr = fontOrStyleForceUpperCase ? originalStr.ToUpper() : originalStr; +#endif + + if (shouldParseRichTextData) + { + RichTextData = Barotrauma.RichTextData.GetRichTextData(NestedStr.Value, out cachedSanitizedValue); + } + else + { + cachedSanitizedValue = NestedStr.Value; + } + if (postProcess != null) { cachedSanitizedValue = postProcess(cachedSanitizedValue); } + language = GameSettings.CurrentConfig.Language; + loaded = NestedStr.Loaded; + } + +#if CLIENT + public RichString CaseTiedToFontAndStyle(GUIFont? font, GUIComponentStyle? componentStyle) + { + return new RichString(originalStr, shouldParseRichTextData, postProcess, font, componentStyle); + } +#endif + + public RichString ToUpper() + { + return new RichString(NestedStr.ToUpper(), shouldParseRichTextData, postProcess); + } + + public RichString ToLower() + { + return new RichString(NestedStr.ToLower(), shouldParseRichTextData, postProcess); + } + + public RichString Replace(string from, string to, StringComparison stringComparison = StringComparison.Ordinal) + { + return new RichString(NestedStr.Replace(from, to, stringComparison), shouldParseRichTextData, postProcess); + } + + public override string ToString() + { + return SanitizedValue; + } + + public bool Contains(string str, StringComparison stringComparison = StringComparison.Ordinal) => + SanitizedValue.Contains(str, stringComparison); + + public bool Contains(char chr, StringComparison stringComparison = StringComparison.Ordinal) => + SanitizedValue.Contains(chr, stringComparison); + + + public static bool operator ==(RichString? a, RichString? b) + => a?.SanitizedValue == b?.SanitizedValue +#if CLIENT + && a?.font == b?.font + && a?.componentStyle == b?.componentStyle +#endif + ; + + public static bool operator !=(RichString? a, RichString? b) => !(a == b); + + public static bool operator ==(RichString? a, LocalizedString? b) + => a?.SanitizedValue == b?.Value; + + public static bool operator !=(RichString? a, LocalizedString? b) => !(a == b); + + public static bool operator ==(LocalizedString? a, RichString? b) + => a?.Value == b?.SanitizedValue; + + public static bool operator !=(LocalizedString? a, RichString? b) => !(a == b); + + public static bool operator ==(RichString? a, string? b) + => a?.SanitizedValue == b; + + public static bool operator !=(RichString? a, string? b) => !(a == b); + + public static bool operator ==(string? a, RichString? b) + => a == b?.SanitizedValue; + + public static bool operator !=(string? a, RichString? b) => !(a == b); + } + + public class StripRichTagsLString : LocalizedString + { + public readonly RichString RichStr; + + public StripRichTagsLString(RichString richStr) + { + RichStr = richStr; + } + + public override bool Loaded => RichStr.NestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = RichStr.SanitizedValue; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs new file mode 100644 index 000000000..7f6743010 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -0,0 +1,404 @@ +#nullable enable + +using Barotrauma.IO; +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace Barotrauma +{ + public enum FormatCapitals + { + Yes, No + } + + public static class TextManager + { + public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier(); + public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); + public static IEnumerable AvailableLanguages => TextPacks.Keys; + + private readonly static Dictionary> cachedStrings = + new Dictionary>(); + private static ImmutableHashSet nonCacheableTags = + ImmutableHashSet.Empty; + + public static int LanguageVersion { get; private set; } = 0; + + private readonly static Regex isCJK = new Regex( + @"\p{IsHangulJamo}|" + + @"\p{IsHiragana}|" + + @"\p{IsKatakana}|" + + @"\p{IsCJKRadicalsSupplement}|" + + @"\p{IsCJKSymbolsandPunctuation}|" + + @"\p{IsEnclosedCJKLettersandMonths}|" + + @"\p{IsCJKCompatibility}|" + + @"\p{IsCJKUnifiedIdeographsExtensionA}|" + + @"\p{IsCJKUnifiedIdeographs}|" + + @"\p{IsHangulSyllables}|" + + @"\p{IsCJKCompatibilityForms}"); + + /// + /// Does the string contain symbols from Chinese, Japanese or Korean languages + /// + public static bool IsCJK(LocalizedString text) + { + return IsCJK(text.Value); + } + + public static bool IsCJK(string text) + { + if (string.IsNullOrEmpty(text)) { return false; } + return isCJK.IsMatch(text); + } + + public static bool ContainsTag(string tag) + { + return ContainsTag(tag.ToIdentifier()); + } + + public static bool ContainsTag(Identifier tag) + { + return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag)); + } + + public static IEnumerable GetAll(string tag) + => GetAll(tag.ToIdentifier()); + + public static IEnumerable GetAll(Identifier tag) + { + return TextPacks[GameSettings.CurrentConfig.Language] + .SelectMany(p => p.Texts.TryGetValue(tag, out var value) + ? (IEnumerable)value + : Array.Empty()); + } + + public static IEnumerable> GetAllTagTextPairs() + { + return TextPacks[GameSettings.CurrentConfig.Language] + .SelectMany(p => p.Texts) + .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v))); + } + + public static IEnumerable GetTextFiles() + { + return GetTextFilesRecursive(Path.Combine("Content", "Texts")); + } + + private static IEnumerable GetTextFilesRecursive(string directory) + { + foreach (string file in Directory.GetFiles(directory)) + { + yield return file.CleanUpPath(); + } + foreach (string subDir in Directory.GetDirectories(directory)) + { + foreach (string file in GetTextFilesRecursive(subDir)) + { + yield return file; + } + } + } + + public static string GetTranslatedLanguageName(LanguageIdentifier languageIdentifier) + { + return TextPacks[languageIdentifier].First().TranslatedName; + } + + public static void ClearCache() + { + lock (cachedStrings) + { + cachedStrings.Clear(); + nonCacheableTags.Clear(); + } + } + + public static LocalizedString Get(params Identifier[] tags) + { + TagLString? str = null; + lock (cachedStrings) + { + if (tags.Length == 1 && !nonCacheableTags.Contains(tags[0])) + { + var tag = tags[0]; + if (cachedStrings.TryGetValue(tag, out var strRef)) + { + if (!strRef.TryGetTarget(out str)) + { + cachedStrings.Remove(tag); + } + } + + if (str is null && TextPacks.ContainsKey(GameSettings.CurrentConfig.Language)) + { + int count = 0; + foreach (var pack in TextPacks[GameSettings.CurrentConfig.Language]) + { + if (pack.Texts.TryGetValue(tag, out var texts)) + { + count += texts.Length; + if (count > 1) { break; } + } + } + + if (count > 1) + { + nonCacheableTags = nonCacheableTags.Add(tag); + } + else + { + str = new TagLString(tags); + cachedStrings.Add(tag, new WeakReference(str)); + } + } + } + } + return str ?? new TagLString(tags); + } + + public static LocalizedString Get(params string[] tags) + => Get(tags.ToIdentifiers()); + + public static LocalizedString AddPunctuation(char punctuationSymbol, params LocalizedString[] texts) + { + return new AddedPunctuationLString(punctuationSymbol, texts); + } + + public static LocalizedString GetFormatted(Identifier tag, params object[] args) + { + return GetFormatted(new TagLString(tag), args); + } + + public static LocalizedString GetFormatted(LocalizedString str, params object[] args) + { + LocalizedString[] argStrs = new LocalizedString[args.Length]; + for (int i = 0; i < args.Length; i++) + { + if (args[i] is LocalizedString ls) { argStrs[i] = ls; } + else { argStrs[i] = new RawLString(args[i].ToString() ?? ""); } + } + return new FormattedLString(str, argStrs); + } + + public static string FormatServerMessage(string str) => $"{str}~"; + + public static string FormatServerMessage(string message, params (string Key, string Value)[] keysWithValues) + { + if (keysWithValues.Length == 0) + { + return FormatServerMessage(message); + } + var startIndex = message.LastIndexOf('/') + 1; + var endIndex = message.IndexOf('~', startIndex); + if (endIndex == -1) + { + endIndex = message.Length - 1; + } + var textId = message.Substring(startIndex, endIndex - startIndex + 1); + var prefixEntries = keysWithValues.Select((kv, index) => + { + if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1) + { + var kvStartIndex = kv.Value.LastIndexOf('/') + 1; + return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}"; + } + else + { + return null; + } + }).Where(e => e != null).ToArray(); + return string.Join("", + (prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""), + message, + string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}")) + ); + } + + internal static string FormatServerMessageWithPronouns(CharacterInfo charInfo, string message, params (string Key, string Value)[] keysWithValues) + { + var pronounCategory = charInfo.Prefab.Pronouns; + (string Key, string Value)[] pronounKwv = new[] + { + ("[PronounLowercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]Lowercase")), + ("[PronounUppercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]")), + ("[PronounPossessiveLowercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]Lowercase")), + ("[PronounPossessiveUppercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]")), + ("[PronounReflexiveLowercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]Lowercase")), + ("[PronounReflexiveUppercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]")) + }; + return FormatServerMessage(message, keysWithValues.Concat(pronounKwv).ToArray()); + } + + // Same as string.Join(separator, parts) but performs the operation taking into account server message string replacements. + public static string JoinServerMessages(string separator, string[] parts, string namePrefix = "part.") + { + + return string.Join("/", + string.Join("/", parts.Select((part, index) => + { + var partStart = part.LastIndexOf('/') + 1; + return partStart > 0 ? $"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $"[{namePrefix}{index}]={part.Substring(partStart)}"; + })), + string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]"))); + } + + public static LocalizedString ParseInputTypes(LocalizedString str) + { + return new InputTypeLString(str); + } + + public static LocalizedString GetWithVariable(string tag, string varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No) + { + return GetWithVariable(tag.ToIdentifier(), varName.ToIdentifier(), value, formatCapitals); + } + + public static LocalizedString GetWithVariable(Identifier tag, Identifier varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No) + { + return GetWithVariables(tag, (varName, value)); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, string Value)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + (LocalizedString)new RawLString(kv.Value), + FormatCapitals.No))); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + kv.Value, + FormatCapitals.No))); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value, FormatCapitals FormatCapitals)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + kv.Value, + kv.FormatCapitals))); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, string Value, FormatCapitals FormatCapitals)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + (LocalizedString)new RawLString(kv.Value), + kv.FormatCapitals))); + } + + public static LocalizedString GetWithVariables(Identifier tag, params (Identifier Key, LocalizedString Value)[] replacements) + { + return GetWithVariables(tag, replacements.Select(kv => (kv.Key, kv.Value, FormatCapitals.No))); + } + + public static LocalizedString GetWithVariables(Identifier tag, IEnumerable<(Identifier, LocalizedString, FormatCapitals)> replacements) + { + return new ReplaceLString(new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements); + } + + public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement) + { + /* + + + + + + */ + + LocalizedString extraDescriptionLine = Get(descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty)); + foreach (XElement replaceElement in descriptionElement.Elements()) + { + if (replaceElement.NameAsIdentifier() != "replace") { continue; } + + Identifier tag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty); + string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty()); + LocalizedString replacementValue = string.Empty; + for (int i = 0; i < replacementValues.Length; i++) + { + replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]); + if (i < replacementValues.Length - 1) + { + replacementValue += ", "; + } + } + if (replaceElement.Attribute("color") != null) + { + string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); + replacementValue = $"‖color:{colorStr}‖" + replacementValue + "‖color:end‖"; + } + extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); + } + if (!(description is RawLString { Value: "" })) { description += "\n"; } //TODO: this is cursed + description += extraDescriptionLine; + } + + public static LocalizedString GetServerMessage(string serverMessage) + { + return new ServerMsgLString(serverMessage); + } + + public static LocalizedString Capitalize(this LocalizedString str) + { + return new CapitalizeLString(str); + } + + public static void IncrementLanguageVersion() + { + LanguageVersion++; + ClearCache(); + } + +#if DEBUG + public static void CheckForDuplicates(LanguageIdentifier lang) + { + if (!TextPacks.ContainsKey(lang)) + { + DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); + return; + } + + int packIndex = 0; + foreach (TextPack textPack in TextPacks[lang]) + { + textPack.CheckForDuplicates(packIndex); + packIndex++; + } + } + + public static void WriteToCSV() + { + LanguageIdentifier lang = DefaultLanguage; + + if (!TextPacks.ContainsKey(lang)) + { + DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); + return; + } + + int packIndex = 0; + foreach (TextPack textPack in TextPacks[lang]) + { + textPack.WriteToCSV(packIndex); + packIndex++; + } + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs new file mode 100644 index 000000000..b4829e7ba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -0,0 +1,174 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + public readonly struct LanguageIdentifier + { + public static readonly LanguageIdentifier None = "None".ToLanguageIdentifier(); + + public readonly Identifier Value; + public int ValueHash => Value.GetHashCode(); + public LanguageIdentifier(Identifier value) { Value = value; } + + public override bool Equals(object obj) + { + if (obj is LanguageIdentifier other) { return this == other; } + return base.Equals(obj); + } + + public override int GetHashCode() => ValueHash; + + public static bool operator ==(LanguageIdentifier a, LanguageIdentifier b) => a.Value == b.Value; + public static bool operator !=(LanguageIdentifier a, LanguageIdentifier b) => !(a==b); + + public override string ToString() => Value.ToString(); + } + + public static class LanguageIdentifierExtensions + { + public static LanguageIdentifier ToLanguageIdentifier(this Identifier identifier) + { + return new LanguageIdentifier(identifier); + } + + public static LanguageIdentifier ToLanguageIdentifier(this string str) + { + return str.ToIdentifier().ToLanguageIdentifier(); + } + } + + public class TextPack + { + public readonly TextFile ContentFile; + + public readonly LanguageIdentifier Language; + + public readonly ImmutableDictionary> Texts; + public readonly string TranslatedName; + public readonly bool NoWhitespace; + + public TextPack(TextFile file, ContentXElement mainElement, LanguageIdentifier language) + { + ContentFile = file; + + var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value); + Language = language; + TranslatedName = mainElement.GetAttributeString("translatedname", languageName.Value); + NoWhitespace = mainElement.GetAttributeBool("nowhitespace", false); + + Dictionary> texts = new Dictionary>(); + foreach (var element in mainElement.Elements()) + { + Identifier elemName = element.NameAsIdentifier(); + if (!texts.ContainsKey(elemName)) { texts.Add(elemName, new List()); } + texts[elemName].Add(element.ElementInnerText() + .Replace(@"\n", "\n") + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(""", "\"") + .Replace("'", "'")); + } + Texts = texts.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + } + +#if DEBUG + public void CheckForDuplicates(int index) + { + Dictionary tagCounts = new Dictionary(); + Dictionary contentCounts = new Dictionary(); + + XDocument doc = XMLExtensions.TryLoadXml(ContentFile.Path); + if (doc == null) { return; } + + foreach (var subElement in doc.Root.Elements()) + { + Identifier infoName = subElement.NameAsIdentifier(); + if (!tagCounts.ContainsKey(infoName)) + { + tagCounts.Add(infoName, 1); + } + else + { + tagCounts[infoName] += 1; + } + + string infoContent = subElement.Value; + if (string.IsNullOrEmpty(infoContent)) continue; + if (!contentCounts.ContainsKey(infoContent)) + { + contentCounts.Add(infoContent, 1); + } + else + { + contentCounts[infoContent] += 1; + } + } + + StringBuilder sb = new StringBuilder(); + sb.Append("Language: " + Language); + sb.AppendLine(); + sb.Append("Duplicate tags:"); + sb.AppendLine(); + sb.AppendLine(); + + for (int i = 0; i < tagCounts.Keys.Count; i++) + { + if (tagCounts[Texts.Keys.ElementAt(i)] > 1) + { + sb.Append(Texts.Keys.ElementAt(i) + " | Count: " + tagCounts[Texts.Keys.ElementAt(i)]); + sb.AppendLine(); + } + } + + sb.AppendLine(); + sb.AppendLine(); + sb.Append("Duplicate content:"); + sb.AppendLine(); + sb.AppendLine(); + + for (int i = 0; i < contentCounts.Keys.Count; i++) + { + if (contentCounts[contentCounts.Keys.ElementAt(i)] > 1) + { + sb.Append(contentCounts.Keys.ElementAt(i) + " | Count: " + contentCounts[contentCounts.Keys.ElementAt(i)]); + sb.AppendLine(); + } + } + + Barotrauma.IO.File.WriteAllText($"duplicate_{Language.ToString().ToLower()}_{index}.txt", sb.ToString()); + } + + public void WriteToCSV(int index) + { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < Texts.Count; i++) + { + Identifier key = Texts.Keys.ElementAt(i); + Texts.TryGetValue(key, out ImmutableArray infoList); + + for (int j = 0; j < infoList.Length; j++) + { + sb.Append(key); // ID + sb.Append('*'); + sb.Append(infoList[j]); // Original + sb.Append('*'); + // Translated + sb.Append('*'); + // Comments + sb.AppendLine(); + } + } + + Barotrauma.IO.File.WriteAllText($"csv_{Language.ToString().ToLower()}_{index}.csv", sb.ToString()); + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs deleted file mode 100644 index 6def082f0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ /dev/null @@ -1,951 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Barotrauma.Extensions; -using System.Xml.Linq; - -namespace Barotrauma -{ - public static class TextManager - { - //only used if none of the selected content packages contain any text files - const string VanillaTextFilePath = "Content/Texts/English/EnglishVanilla.xml"; - - private static readonly object mutex = new object(); - - //key = language - private static Dictionary> textPacks; - - private static readonly string[] serverMessageCharacters = new string[] { "~", "[", "]", "=" }; - - public static string Language; - - public static bool Initialized - { - get; - private set; - } - - private static HashSet availableLanguages; - public static IEnumerable AvailableLanguages - { - get - { - lock (mutex) - { - return new HashSet(availableLanguages); - } - } - } - - public static List GetTextFiles() - { - var list = new List(); - GetTextFilesRecursive(Path.Combine("Content", "Texts"), ref list); - return list; - } - - private static void GetTextFilesRecursive(string directory, ref List list) - { - foreach (string file in Directory.GetFiles(directory)) - { - list.Add(file); - } - foreach (string subDir in Directory.GetDirectories(directory)) - { - GetTextFilesRecursive(subDir, ref list); - } - } - - /// - /// Returns the name of the language in the respective language - /// - public static string GetTranslatedLanguageName(string language) - { - lock (mutex) - { - if (!textPacks.ContainsKey(language)) - { - return language; - } - - foreach (var textPack in textPacks[language]) - { - if (textPack.Language == language) - { - return textPack.TranslatedName; - } - } - return language; - } - } - - public static void LoadTextPacks(IEnumerable selectedContentPackages) - { - HashSet newLanguages = new HashSet(); - Dictionary> newTextPacks = new Dictionary>(); - - var textFiles = ContentPackage.GetFilesOfType(selectedContentPackages, ContentType.Text).ToList(); - - foreach (ContentFile file in textFiles) - { -#if !DEBUG - try - { -#endif - var textPack = new TextPack(file.Path); - newLanguages.Add(textPack.Language); - if (!newTextPacks.ContainsKey(textPack.Language)) - { - newTextPacks.Add(textPack.Language, new List()); - } - newTextPacks[textPack.Language].Add(textPack); -#if !DEBUG - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to load text file \"" + file.Path + "\"!", e); - } -#endif - } - - if (newTextPacks.Count == 0) - { - DebugConsole.ThrowError("No text files available in any of the selected content packages. Attempting to find a vanilla English text file..."); - if (!File.Exists(VanillaTextFilePath)) - { - throw new Exception("No text files found in any of the selected content packages or in the default text path!"); - } - var textPack = new TextPack(VanillaTextFilePath); - newLanguages.Add(textPack.Language); - newTextPacks.Add(textPack.Language, new List() { textPack }); - } - - if (newTextPacks.Count == 0) - { - throw new Exception("Failed to load text packs!"); - } - - lock (mutex) - { - textPacks = newTextPacks; - availableLanguages = newLanguages; - DebugConsole.NewMessage("Loaded languages: " + string.Join(", ", newLanguages)); - } - - Initialized = true; - } - - public static void LoadTextPack(string file) - { - lock (mutex) - { - var textPack = new TextPack(file); - availableLanguages.Add(textPack.Language); - if (!textPacks.ContainsKey(textPack.Language)) - { - textPacks.Add(textPack.Language, new List()); - } - textPacks[textPack.Language].Add(textPack); - } - } - - public static void RemoveTextPack(string file) - { - List keysToRemove = new List(); - foreach (var textPackKVP in textPacks) - { - var textPackLanguage = textPackKVP.Key; - var textPackList = textPackKVP.Value; - for (int i = 0; i < textPackList.Count; i++) - { - if (textPackList[i].FilePath == file) - { - textPackList.Remove(textPackList[i]); - if (textPackList.Count == 0) - { - keysToRemove.Add(textPackLanguage); - } - } - } - } - - foreach (var key in keysToRemove) - { - availableLanguages.Remove(key); - textPacks.Remove(key); - } - } - - public static bool ContainsTag(string textTag) - { - if (string.IsNullOrEmpty(textTag)) { return false; } - - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - foreach (TextPack textPack in textPacks[Language]) - { - if (textPack.Get(textTag) != null) { return true; } - } - } - - return false; - } - - private static readonly List availableTexts = new List(); - - public static string Get(string textTag, bool returnNull = false, string fallBackTag = null, bool useEnglishAsFallBack = true) - { - lock (mutex) - { - if (textPacks == null) - { - DebugConsole.ThrowError($"Failed to get the text \"{textTag}\" (no text packs loaded)."); - return textTag; - } - - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - -#if DEBUG - if (GameMain.Config != null && GameMain.Config.TextManagerDebugModeEnabled) - { - return textTag; - } -#endif - availableTexts.Clear(); - foreach (TextPack textPack in textPacks[Language]) - { - var texts = textPack.GetAll(textTag); - if (texts != null) - { - availableTexts.AddRange(texts); - } - } - - if (availableTexts.Any()) - { - return availableTexts.GetRandom().Replace(@"\n", "\n"); - } - - if (!string.IsNullOrEmpty(fallBackTag)) - { - foreach (TextPack textPack in textPacks[Language]) - { - string text = textPack.Get(fallBackTag); - if (text != null) { return text; } - } - } - - //if text was not found and we're using a language other than English, see if we can find an English version - //may happen, for example, if a user has selected another language and using mods that haven't been translated to that language - if (useEnglishAsFallBack && Language != "English" && textPacks.ContainsKey("English")) - { - foreach (TextPack textPack in textPacks["English"]) - { - string text = textPack.Get(textTag); - if (text != null) - { -#if DEBUG - DebugConsole.NewMessage("Text \"" + textTag + "\" not found for the language \"" + Language + "\". Using the English text \"" + text + "\" instead."); -#endif - return text; - } - } - } - - if (returnNull) - { - return null; - } - else - { - DebugConsole.ThrowError("Text \"" + textTag + "\" not found."); - return textTag; - } - } - } - - public static string GetWithVariables(string textTag, string[] variableTags, string[] variableValues, bool[] formatCapitals = null, bool returnNull = false, string fallBackTag = null) - { - string text = Get(textTag, returnNull, fallBackTag); - - if (text == null || text.Length == 0 || variableTags.Length != variableValues.Length) - { -#if DEBUG - if (variableTags.Length != variableValues.Length) - { - DebugConsole.ThrowError("variableTags.Length and variableValues.Length do not match for \"" + textTag + "\"."); - } - - if (formatCapitals != null && formatCapitals.Length != variableTags.Length) - { - DebugConsole.ThrowError("variableTags.Length and formatCapitals.Length do not match for \"" + textTag + "\"."); - } -#endif - if (returnNull) - { - return null; - } - else - { - return textTag; - } - } - - if (formatCapitals != null && (GameMain.Config == null || !GameMain.Config.Language.Contains("Chinese"))) - { - for (int i = 0; i < variableTags.Length; i++) - { - if (string.IsNullOrEmpty(variableValues[i])) { continue; } - if (formatCapitals[i]) - { - variableValues[i] = HandleVariableCapitalization(text, variableTags[i], variableValues[i]); - } - } - } - - for (int i = 0; i < variableTags.Length; i++) - { - if (variableValues[i] == null) - { -#if DEBUG - DebugConsole.ThrowError("Error in TextManager.GetWithVariables (variable " + i + " was null).\n" + Environment.StackTrace.CleanupStackTrace()); -#endif - continue; - } - text = text.Replace(variableTags[i], variableValues[i]); - } - - return text; - } - - public static string GetWithVariable(string textTag, string variableTag, string variableValue, bool formatCapitals = false, bool returnNull = false, string fallBackTag = null) - { - string text = Get(textTag, returnNull, fallBackTag); - - if (text == null || text.Length == 0) - { - if (returnNull) - { - return null; - } - else - { - return textTag; - } - } - - if (variableValue == null) - { - variableValue = "null"; -#if DEBUG - throw new ArgumentException($"Variable value \"{variableTag}\" was null."); -#endif - } - - if (formatCapitals && !GameMain.Config.Language.Contains("Chinese")) - { - variableValue = HandleVariableCapitalization(text, variableTag, variableValue); - } - - return text.Replace(variableTag, variableValue); - } - - private static string HandleVariableCapitalization(string text, string variableTag, string variableValue) - { - int index = text.IndexOf(variableTag) - 1; - if (index == -1) - { - return variableValue; - } - - for (int i = index; i >= 0; i--) - { - if (text[i] == ' ') - { - continue; - } - else - { - if (text[i] != '.') - { - variableValue = variableValue.ToLower(); - } - else - { - variableValue = Capitalize(variableValue); - break; - } - } - } - - return variableValue; - } - - //TODO: the server should not be doing this! - public static string ParseInputTypes(string text) - { -#if CLIENT - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - text = text.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBindText(inputType)); - text = text.Replace("[InputType." + inputType.ToString() + "]", GameMain.Config.KeyBindText(inputType)); - } -#endif - return text; - } - - public static string GetFormatted(string textTag, bool returnNull = false, params object[] args) - { - string text = Get(textTag, returnNull); - - if (text == null || text.Length == 0) - { - if (returnNull) - { - return null; - } - else - { - DebugConsole.ThrowError("Text \"" + textTag + "\" not found."); - return textTag; - } - } - - try - { - return string.Format(text, args); - } - catch (FormatException) - { - string errorMsg = "Failed to format text \"" + text + "\", args: " + string.Join(", ", args); - GameAnalyticsManager.AddErrorEventOnce("TextManager.GetFormatted:FormatException", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return text; - } - } - - /// - /// Constructs a string from XML in a way that allows replacing one or more variables with hard-coded or localized values. Usage example in the method's comments. - /// - public static void ConstructDescription(ref string Description, XElement descriptionElement) - { - /* - - - - - - */ - - if (descriptionElement.GetAttributeBool("linebreak", false)) - { - Description += "\n"; - return; - } - - string descriptionTag = descriptionElement.GetAttributeString("tag", string.Empty); - string extraDescriptionLine = Get(descriptionTag); - if (string.IsNullOrEmpty(extraDescriptionLine)) { return; } - foreach (XElement replaceElement in descriptionElement.Elements()) - { - if (replaceElement.Name.ToString().ToLowerInvariant() != "replace") { continue; } - - string tag = replaceElement.GetAttributeString("tag", string.Empty); - string[] replacementValues = replaceElement.GetAttributeStringArray("value", new string[0]); - string replacementValue = string.Empty; - for (int i = 0; i < replacementValues.Length; i++) - { -#if DEBUG - if (!int.TryParse(replacementValues[i], out int _) && !float.TryParse(replacementValues[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out float __) && !ContainsTag(replacementValues[i])) - { - DebugConsole.AddWarning($"Couldn't find the tag \"{replacementValues[i]}\" in text files for description \"{descriptionTag}\". Is the tag correct?"); - } -#endif - replacementValue += Get(replacementValues[i], returnNull: true) ?? replacementValues[i]; - if (i < replacementValues.Length - 1) - { - replacementValue += ", "; - } - } - if (replaceElement.Attribute("color") != null) - { - string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); - replacementValue = $"‖color:{colorStr}‖{replacementValue}‖color:end‖"; - } - extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); - } - if (!string.IsNullOrEmpty(Description)) { Description += "\n"; } - Description += extraDescriptionLine; - } - - public static string FormatServerMessage(string textId) - { - return $"{textId}~"; - } - - public static string FormatServerMessage(string message, IEnumerable keys, IEnumerable values) - { - if (keys == null || values == null || !keys.Any() || !values.Any()) - { - return FormatServerMessage(message); - } - var startIndex = message.LastIndexOf('/') + 1; - var endIndex = message.IndexOf('~', startIndex); - if (endIndex == -1) - { - endIndex = message.Length - 1; - } - var textId = message.Substring(startIndex, endIndex - startIndex + 1); - var keysWithValues = keys.Zip(values, (key, value) => new { Key = key, Value = value }); - var prefixEntries = keysWithValues.Select((kv, index) => - { - if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1) - { - var kvStartIndex = kv.Value.LastIndexOf('/') + 1; - return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}"; - } - else - { - return null; - } - }).Where(e => e != null).ToArray(); - return string.Join("", - (prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""), - message, - string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}").ToArray()) - ); - } - - static readonly string[] genderPronounVariables = { - "[genderpronoun]", - "[genderpronounpossessive]", - "[genderpronounreflexive]", - "[Genderpronoun]", - "[Genderpronounpossessive]", - "[Genderpronounreflexive]" - }; - - static readonly string[] genderPronounMaleValues = { - "PronounMaleLowercase", - "PronounPossessiveMaleLowercase", - "PronounReflexiveMaleLowercase", - "PronounMale", - "PronounPossessiveMale", - "PronounReflexiveMale" - }; - - static readonly string[] genderPronounFemaleValues = { - "PronounFemaleLowercase", - "PronounPossessiveFemaleLowercase", - "PronounReflexiveFemaleLowercase", - "PronounMale", - "PronounPossessiveFemale", - "PronounReflexiveFemale" - }; - - public static string FormatServerMessageWithGenderPronouns(Gender gender, string message, IEnumerable keys, IEnumerable values) - { - return FormatServerMessage(message, keys.Concat(genderPronounVariables), values.Concat(gender == Gender.Male ? genderPronounMaleValues : genderPronounFemaleValues)); - } - - // Same as string.Join(separator, parts) but performs the operation taking into account server message string replacements. - public static string JoinServerMessages(string separator, string[] parts, string namePrefix = "part.") - { - - return string.Join("/", - string.Join("/", parts.Select((part, index) => - { - var partStart = part.LastIndexOf('/') + 1; - return partStart > 0 ? $"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $"[{namePrefix}{index}]={part.Substring(partStart)}"; - })), - string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]"))); - } - - static readonly Regex reFormattedMessage = new Regex(@"^(?[\[\].a-z0-9_]+?)=(?[a-z0-9_]+?)\((?.+?)\)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - static readonly Regex reReplacedMessage = new Regex(@"^(?[\[\].a-z0-9_]+?)=(?.*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - static readonly Dictionary> messageFormatters = new Dictionary> - { - { "duration", secondsValue => double.TryParse(secondsValue, out var seconds) ? $"{TimeSpan.FromSeconds(seconds):g}" : null } - }; - - // Format: ServerMessage.Identifier1/ServerMessage.Indentifier2~[variable1]=value~[variable2]=value - // Also: replacement=ServerMessage.Identifier1~[variable1]=value/ServerMessage.Identifier2~[variable2]=replacement - // And: replacement=formatter(value) - // Variable that requires translation -> ServerMessage.Indentifier1~[variable1]=§value - public static string GetServerMessage(string serverMessage) - { - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - } - - string[] messages = serverMessage.Split('/'); - var replacedMessages = new Dictionary(); - - bool translationsFound = false; - - try - { - for (int i = 0; i < messages.Length; i++) - { - if (messages[i].EndsWith("~", StringComparison.Ordinal)) - { - messages[i] = messages[i].Substring(0, messages[i].Length - 1); - } - if (!IsServerMessageWithVariables(messages[i]) && !messages[i].Contains('=')) // No variables, try to translate - { - foreach (var replacedMessage in replacedMessages) - { - messages[i] = messages[i].Replace(replacedMessage.Key, replacedMessage.Value); - } - - if (messages[i].Contains(" ")) continue; // Spaces found, do not translate - string msg = Get(messages[i], true); - if (msg != null) // If a translation was found, otherwise use the original - { - messages[i] = msg; - translationsFound = true; - } - } - else - { - string messageVariable = null; - var matchFormatted = reFormattedMessage.Match(messages[i]); - if (matchFormatted.Success) - { - var formatter = matchFormatted.Groups["formatter"].ToString(); - if (messageFormatters.TryGetValue(formatter, out var formatterFn)) - { - var formattedValue = formatterFn(matchFormatted.Groups["value"].ToString()); - if (formattedValue != null) - { - messageVariable = matchFormatted.Groups["variable"].ToString(); - messages[i] = formattedValue; - } - } - } - if (messageVariable == null) - { - var matchReplaced = reReplacedMessage.Match(messages[i]); - if (matchReplaced.Success) - { - messageVariable = matchReplaced.Groups["variable"].ToString(); - messages[i] = matchReplaced.Groups["message"].ToString(); - } - } - - foreach (var replacedMessage in replacedMessages) - { - messages[i] = messages[i].Replace(replacedMessage.Key, replacedMessage.Value); - } - - - string[] messageWithVariables = messages[i].Split('~'); - - string msg = Get(messageWithVariables[0], true); - - if (msg != null) // If a translation was found, otherwise use the original - { - messages[i] = msg; - translationsFound = true; - } - else if (messageVariable == null) - { - continue; // No translation found, probably caused by player input -> skip variable handling - } - - // First index is always the message identifier -> start at 1 - for (int j = 1; j < messageWithVariables.Length; j++) - { - string[] variableAndValue = messageWithVariables[j].Split('='); - messages[i] = messages[i].Replace(variableAndValue[0], variableAndValue[1].Length > 1 && variableAndValue[1][0] == '§' ? Get(variableAndValue[1].Substring(1)) : variableAndValue[1]); - } - - if (messageVariable != null) - { - replacedMessages[messageVariable] = messages[i]; - messages[i] = null; - } - } - } - - if (translationsFound) - { - string translatedServerMessage = string.Empty; - for (int i = 0; i < messages.Length; i++) - { - if (messages[i] != null) - { - translatedServerMessage += messages[i]; - } - } - return translatedServerMessage; - } - else - { - return serverMessage; - } - } - - catch (IndexOutOfRangeException exception) - { - string errorMsg = "Failed to translate server message \"" + serverMessage + "\"."; -#if DEBUG - DebugConsole.ThrowError(errorMsg, exception); -#endif - GameAnalyticsManager.AddErrorEventOnce("TextManager.GetServerMessage:" + serverMessage, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return errorMsg; - } - } - - /// - /// Fetches a single variable from a servermessage - /// - public static string GetServerMessageVariable(string message, string variable) - { - int variableIndex = message.IndexOf(variable); - if (variableIndex == -1) - { -#if DEBUG - DebugConsole.ThrowError($"Server message variable: '{variable}' not found in message: '{message}'"); -#endif - return string.Empty; - } - - int startIndex = message.IndexOf('=', variableIndex) + 1; - int endIndex = startIndex; - - for (int i = startIndex; i < message.Length; i++) - { - if (message[i] == '/' || message[i] == '~') - { - endIndex = i; - break; - } - } - - if (endIndex == startIndex) endIndex = message.Length; - - return message.Substring(startIndex, endIndex - startIndex); - } - - public static bool IsServerMessageWithVariables(string message) - { - for (int i = 0; i < serverMessageCharacters.Length; i++) - { - if (!message.Contains(serverMessageCharacters[i])) return false; - } - - return true; - } - - /// - /// Adds a punctuation symbol between two strings, taking into account special rules in some locales (e.g. non-breaking space before a colon in French) - /// - public static string AddPunctuation(char punctuationSymbol, params string[] texts) - { - string separator = ""; - switch (GameMain.Config.Language) - { - case "French": - bool addNonBreakingSpace = - punctuationSymbol == ':' || punctuationSymbol == ';' || - punctuationSymbol == '!' || punctuationSymbol == '?'; - separator = addNonBreakingSpace ? - new string(new char[] { (char)(0xA0), punctuationSymbol, ' ' }) : - new string(new char[] { punctuationSymbol, ' ' }); - break; - default: - separator = new string(new char[] { punctuationSymbol, ' ' }); - break; - } - return string.Join(separator, texts); - } - - public static List GetAll(string textTag) - { - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - - List allText; - - foreach (TextPack textPack in textPacks[Language]) - { - allText = textPack.GetAll(textTag); - if (allText != null) return allText; - } - - //if text was not found and we're using a language other than English, see if we can find an English version - //may happen, for example, if a user has selected another language and using mods that haven't been translated to that language - if (Language != "English" && textPacks.ContainsKey("English")) - { - foreach (TextPack textPack in textPacks["English"]) - { - allText = textPack.GetAll(textTag); - if (allText != null) return allText; - } - } - - return null; - } - } - - public static List> GetAllTagTextPairs() - { - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - - List> allText = new List>(); - - foreach (TextPack textPack in textPacks[Language]) - { - allText.AddRange(textPack.GetAllTagTextPairs()); - } - - return allText; - } - } - - public static string ReplaceGenderPronouns(string text, Gender gender) - { - if (gender == Gender.Male) - { - return text.Replace("[genderpronoun]", Get("PronounMaleLowercase")) - .Replace("[genderpronounpossessive]", Get("PronounPossessiveMaleLowercase")) - .Replace("[genderpronounreflexive]", Get("PronounReflexiveMaleLowercase")) - .Replace("[Genderpronoun]", Get("PronounMale")) - .Replace("[Genderpronounpossessive]", Get("PronounPossessiveMale")) - .Replace("[Genderpronounreflexive]", Get("PronounReflexiveMale")); - } - else - { - return text.Replace("[genderpronoun]", Get("PronounFemaleLowercase")) - .Replace("[genderpronounpossessive]", Get("PronounPossessiveFemaleLowercase")) - .Replace("[genderpronounreflexive]", Get("PronounReflexiveFemaleLowerCase")) - .Replace("[Genderpronoun]", Get("PronounFemale")) - .Replace("[Genderpronounpossessive]", Get("PronounPossessiveFemale")) - .Replace("[Genderpronounreflexive]", Get("PronounReflexiveFemale")); - } - } - - static Regex isCJK = new Regex( - @"\p{IsHangulJamo}|" + - @"\p{IsHiragana}|" + - @"\p{IsKatakana}|" + - @"\p{IsCJKRadicalsSupplement}|" + - @"\p{IsCJKSymbolsandPunctuation}|" + - @"\p{IsEnclosedCJKLettersandMonths}|" + - @"\p{IsCJKCompatibility}|" + - @"\p{IsCJKUnifiedIdeographsExtensionA}|" + - @"\p{IsCJKUnifiedIdeographs}|" + - @"\p{IsHangulSyllables}|" + - @"\p{IsCJKCompatibilityForms}"); - - /// - /// Does the string contain symbols from Chinese, Japanese or Korean languages - /// - public static bool IsCJK(string text) - { - if (string.IsNullOrEmpty(text)) { return false; } - return isCJK.IsMatch(text); - } - -#if DEBUG - public static void CheckForDuplicates(string lang) - { - lock (mutex) - { - if (!textPacks.ContainsKey(lang)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); - return; - } - - int packIndex = 0; - foreach (TextPack textPack in textPacks[lang]) - { - textPack.CheckForDuplicates(packIndex); - packIndex++; - } - } - } - - public static void WriteToCSV() - { - lock (mutex) - { - string lang = "English"; - - if (!textPacks.ContainsKey(lang)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); - return; - } - - int packIndex = 0; - foreach (TextPack textPack in textPacks[lang]) - { - textPack.WriteToCSV(packIndex); - packIndex++; - } - } - } -#endif - - public static string Capitalize(string str) - { - if (string.IsNullOrWhiteSpace(str)) - { - return str; - } - - return char.ToUpper(str[0]) + str.Substring(1); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs deleted file mode 100644 index 395e5ffa4..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Xml.Linq; -using Barotrauma.IO; - -namespace Barotrauma -{ - class TextPack - { - public readonly string Language; - - /// - /// The name of the language in the language this pack is written in - /// - public readonly string TranslatedName; - - private readonly Dictionary> texts; - - public readonly string FilePath; - - public TextPack(string filePath) - { - this.FilePath = filePath; - texts = new Dictionary>(); - - XDocument doc = null; - for (int i = 0; i < 3; i++) - { - doc = XMLExtensions.TryLoadXml(filePath); - if (doc != null) { break; } - if (filePath.Equals("content/texts/englishvanilla.xml", StringComparison.OrdinalIgnoreCase)) - { - //try fixing legacy EnglishVanilla path - string newPath = "Content/Texts/English/EnglishVanilla.xml"; - if (Barotrauma.IO.File.Exists(newPath)) - { - DebugConsole.NewMessage("Content package is using the obsolete text file path \"" + filePath + "\". Attempting to load from \"" + newPath + "\"..."); - this.FilePath = filePath = newPath; - } - } - Thread.Sleep(1000); - } - if (doc == null) - { - Language = "Unknown"; - return; - } - - Language = doc.Root.GetAttributeString("language", "Unknown"); - TranslatedName = doc.Root.GetAttributeString("translatedname", Language); - - foreach (XElement subElement in doc.Root.Elements()) - { - string infoName = subElement.Name.ToString().ToLowerInvariant(); - if (!texts.TryGetValue(infoName, out List infoList)) - { - infoList = new List(); - texts.Add(infoName, infoList); - } - - string text = subElement.ElementInnerText(); - text = text.Replace("&", "&"); - text = text.Replace("<", "<"); - text = text.Replace(">", ">"); - text = text.Replace(""", "\""); - infoList.Add(text); - } - } - - public string Get(string textTag) - { - if (string.IsNullOrEmpty(textTag)) - { - return null; - } - if (!texts.TryGetValue(textTag.ToLowerInvariant(), out List textList) || !textList.Any()) - { - return null; - } - - string text = textList[Rand.Int(textList.Count)].Replace(@"\n", "\n"); - return text; - } - - public List GetAll(string textTag) - { - if (textTag is null) { return null; } - - if (!texts.TryGetValue(textTag.ToLowerInvariant(), out List textList) || !textList.Any()) - { - return null; - } - - return textList; - } - - public List> GetAllTagTextPairs() - { - var pairs = new List>(); - foreach (KeyValuePair> kvp in texts) - { - foreach (string line in kvp.Value) - { - pairs.Add(new KeyValuePair(kvp.Key, line)); - } - } - - return pairs; - } - -#if DEBUG - public void CheckForDuplicates(int index) - { - Dictionary tagCounts = new Dictionary(); - Dictionary contentCounts = new Dictionary(); - - XDocument doc = XMLExtensions.TryLoadXml(FilePath); - if (doc == null) { return; } - - foreach (XElement subElement in doc.Root.Elements()) - { - string infoName = subElement.Name.ToString().ToLowerInvariant(); - if (!tagCounts.ContainsKey(infoName)) - { - tagCounts.Add(infoName, 1); - } - else - { - tagCounts[infoName] += 1; - } - - string infoContent = subElement.Value; - if (string.IsNullOrEmpty(infoContent)) continue; - if (!contentCounts.ContainsKey(infoContent)) - { - contentCounts.Add(infoContent, 1); - } - else - { - contentCounts[infoContent] += 1; - } - } - - StringBuilder sb = new StringBuilder(); - sb.Append("Language: " + Language); - sb.AppendLine(); - sb.Append("Duplicate tags:"); - sb.AppendLine(); - sb.AppendLine(); - - for (int i = 0; i < tagCounts.Keys.Count; i++) - { - if (tagCounts[texts.Keys.ElementAt(i)] > 1) - { - sb.Append(texts.Keys.ElementAt(i) + " | Count: " + tagCounts[texts.Keys.ElementAt(i)]); - sb.AppendLine(); - } - } - - sb.AppendLine(); - sb.AppendLine(); - sb.Append("Duplicate content:"); - sb.AppendLine(); - sb.AppendLine(); - - for (int i = 0; i < contentCounts.Keys.Count; i++) - { - if (contentCounts[contentCounts.Keys.ElementAt(i)] > 1) - { - sb.Append(contentCounts.Keys.ElementAt(i) + " | Count: " + contentCounts[contentCounts.Keys.ElementAt(i)]); - sb.AppendLine(); - } - } - - File.WriteAllText(@"duplicate_" + Language.ToLower() + "_" + index + ".txt", sb.ToString()); - } - - public void WriteToCSV(int index) - { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < texts.Count; i++) - { - string key = texts.Keys.ElementAt(i); - texts.TryGetValue(key, out List infoList); - - for (int j = 0; j < infoList.Count; j++) - { - sb.Append(key); // ID - sb.Append('*'); - sb.Append(infoList[j]); // Original - sb.Append('*'); - // Translated - sb.Append('*'); - // Comments - sb.AppendLine(); - } - } - - File.WriteAllText(@"csv_" + Language.ToLower() + "_" + index + ".csv", sb.ToString()); - } -#endif - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 6685423d4..60a8364ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -14,25 +14,10 @@ namespace Barotrauma public const double Step = 1.0 / FixedUpdateRate; public const double AccumulatorMax = 0.25f; - private static int frameLimit; /// /// Maximum FPS (0 = unlimited). /// - public static int FrameLimit - { - get { return frameLimit; } - set - { - if (value <= 0) - { - frameLimit = 0; - } - else - { - frameLimit = Math.Max(value, FixedUpdateRate); - } - } - } + public static int FrameLimit => GameSettings.CurrentConfig.Graphics.FrameLimit; public static double Alpha { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs index 20a23537a..a8e58b990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs @@ -6,7 +6,7 @@ namespace Barotrauma { public readonly string EndMessage; - public readonly string MissionIdentifier; + public readonly Identifier MissionIdentifier; public readonly bool Success; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index 41039f752..f8d4a1d2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public object? OriginalValue { get; private set; } - public readonly string Name; + public readonly Identifier Name; private readonly string Multiplier; @@ -22,7 +22,7 @@ namespace Barotrauma private readonly Upgrade upgrade; - private PropertyReference(string name, string multiplier, Upgrade upgrade) + private PropertyReference(Identifier name, string multiplier, Upgrade upgrade) { this.Name = name; this.Multiplier = multiplier; @@ -114,7 +114,7 @@ namespace Barotrauma } else { - float multiplier = UpgradePrefab.ParsePercentage(Multiplier, "", suppressWarnings: true); + float multiplier = UpgradePrefab.ParsePercentage(Multiplier, Identifier.Empty, suppressWarnings: true); return ApplyPercentage(value, multiplier, level); } } @@ -131,7 +131,7 @@ namespace Barotrauma foreach (var savedValue in savedElement.Elements()) { - if (string.Equals(savedValue.Name.ToString(), Name, StringComparison.OrdinalIgnoreCase)) + if (savedValue.NameAsIdentifier() == Name) { OriginalValue = savedValue.GetAttributeFloat("value", 0.0f); } @@ -152,7 +152,7 @@ namespace Barotrauma public static PropertyReference[] ParseAttributes(IEnumerable attributes, Upgrade upgrade) { - return attributes.Select(attribute => new PropertyReference(attribute.Name.ToString(), attribute.Value, upgrade)).ToArray(); + return attributes.Select(attribute => new PropertyReference(attribute.NameAsIdentifier(), attribute.Value, upgrade)).ToArray(); } private float ParseValue() @@ -186,13 +186,13 @@ namespace Barotrauma public UpgradePrefab Prefab { get; } - public string Identifier => Prefab.Identifier; + public Identifier Identifier => Prefab.Identifier; public int Level { get; set; } public bool Disposed { get; private set; } - private readonly XElement sourceElement; + private readonly ContentXElement sourceElement; public Upgrade(ISerializableEntity targetEntity, UpgradePrefab prefab, int level, XContainer? saveElement = null) { @@ -205,7 +205,7 @@ namespace Barotrauma List? saveElements = saveElement?.Elements().ToList(); - foreach (XElement subElement in prefab.SourceElement.Elements()) + foreach (var subElement in prefab.SourceElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -275,11 +275,12 @@ namespace Barotrauma { if (TargetComponents.SelectMany(pair => pair.Value) .Select(@ref => @ref.Name) - .Any(@string => string.Equals(@string, element.Name.ToString(), StringComparison.OrdinalIgnoreCase))) { continue; } + .Any(@identifier => @identifier == element.NameAsIdentifier())) { continue; } string value = element.GetAttributeString("value", string.Empty); - string name = element.Name.ToString(); - string componentName = element.Parent.Name.ToString(); + Identifier name = element.NameAsIdentifier(); + XElement parentElement = element.Parent ?? throw new NullReferenceException("Unable to reset properties: Parent element is null."); + string componentName = parentElement.Name.ToString(); DebugConsole.AddWarning($"Upgrade \"{Prefab.Name}\" in {TargetEntity.Name} does not affect the property \"{name}\" but the save file suggest it has done so before (has it been overriden?). \n" + $"The property has been reset to the original value of {value} and will be ignored from now on."); @@ -339,12 +340,12 @@ namespace Barotrauma string name = key is ItemComponent ? key.Name : "This"; - XElement subElement = new XElement(name); + var subElement = new XElement(name); foreach (PropertyReference propertyRef in value) { if (propertyRef.OriginalValue != null) { - subElement.Add(new XElement(propertyRef.Name, + subElement.Add(new XElement(propertyRef.Name.Value, new XAttribute("value", propertyRef.OriginalValue))); } else if (!Prefab.SuppressWarnings) @@ -390,10 +391,10 @@ namespace Barotrauma int closestMatch = int.MaxValue; foreach (var (propertyName, _) in entity.SerializableProperties) { - int match = ToolBox.LevenshteinDistance(propertyName, propertyReference.Name); + int match = ToolBox.LevenshteinDistance(propertyName.Value, propertyReference.Name.Value); if (match < closestMatch) { - matchingString = propertyName; + matchingString = propertyName.Value ?? ""; closestMatch = match; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 1562443c7..027279c9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -1,9 +1,11 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma @@ -18,15 +20,15 @@ namespace Barotrauma public readonly UpgradePrefab Prefab; - public UpgradePrice(UpgradePrefab prefab, XElement element) + public UpgradePrice(UpgradePrefab prefab, ContentXElement element) { Prefab = prefab; - IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty), - "IncreaseLow", element, suppressWarnings: prefab.SuppressWarnings); + IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty)!, + "IncreaseLow".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings); - IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty), - "IncreaseHigh", element, suppressWarnings: prefab.SuppressWarnings); + IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty)!, + "IncreaseHigh".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings); BasePrice = element.GetAttributeInt("baseprice", -1); @@ -52,43 +54,87 @@ namespace Barotrauma } } - internal class UpgradeCategory + abstract class UpgradeContentPrefab : Prefab { - public static readonly List Categories = new List(); + public static readonly PrefabCollection PrefabsAndCategories = new PrefabCollection( + onAdd: (prefab, isOverride) => + { + if (prefab is UpgradePrefab upgradePrefab) + { + UpgradePrefab.Prefabs.Add(upgradePrefab, isOverride); + } + else if (prefab is UpgradeCategory upgradeCategory) + { + UpgradeCategory.Categories.Add(upgradeCategory, isOverride); + } + }, + onRemove: (prefab) => + { + if (prefab is UpgradePrefab upgradePrefab) + { + UpgradePrefab.Prefabs.Remove(upgradePrefab); + } + else if (prefab is UpgradeCategory upgradeCategory) + { + UpgradeCategory.Categories.Remove(upgradeCategory); + } + }, + onSort: () => + { + UpgradePrefab.Prefabs.SortAll(); + UpgradeCategory.Categories.SortAll(); + }, + onAddOverrideFile: (file) => + { + UpgradePrefab.Prefabs.AddOverrideFile(file); + UpgradeCategory.Categories.AddOverrideFile(file); + }, + onRemoveOverrideFile: (file) => + { + UpgradePrefab.Prefabs.RemoveOverrideFile(file); + UpgradeCategory.Categories.RemoveOverrideFile(file); + }); - public readonly string[] ItemTags; - public readonly string Identifier; + public UpgradeContentPrefab(ContentXElement element, UpgradeModulesFile file) : base(file, element) { } + } + + internal class UpgradeCategory : UpgradeContentPrefab + { + public static readonly PrefabCollection Categories = new PrefabCollection(); + + private readonly ImmutableHashSet selfItemTags; + private readonly HashSet prefabsThatAllowUpgrades = new HashSet(); public readonly bool IsWallUpgrade; - public readonly string Name; + public readonly LocalizedString Name; - public UpgradeCategory(XElement element) + public readonly IEnumerable ItemTags; + + public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file) { - ItemTags = element.GetAttributeStringArray("items", new string[] { }); - Identifier = element.GetAttributeString("identifier", string.Empty); - Name = element.GetAttributeString("name", string.Empty); + selfItemTags = element.GetAttributeIdentifierArray("items", Array.Empty())?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; + Name = element.GetAttributeString("name", string.Empty)!; IsWallUpgrade = element.GetAttributeBool("wallupgrade", false); - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades); - if (!string.IsNullOrWhiteSpace(nameIdentifier)) - { - Name = TextManager.Get($"{nameIdentifier}", returnNull: true) ?? string.Empty; - } - else if (string.IsNullOrWhiteSpace(Name)) - { - Name = TextManager.Get($"UpgradeCategory.{Identifier}", true) ?? string.Empty; - } + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + if (!nameIdentifier.IsEmpty) { - string[] identifierArray = itemPrefab.AllowedUpgrades.Split(","); - if (identifierArray.Contains(Identifier)) - { - ItemTags = ItemTags.Concat(new[] { itemPrefab.Identifier }).ToArray(); - } + Name = TextManager.Get($"{nameIdentifier}"); } + else if (Name.IsNullOrWhiteSpace()) + { + Name = TextManager.Get($"UpgradeCategory.{Identifier}"); + } + } - Categories.Add(this); + public void DeterminePrefabsThatAllowUpgrades() + { + prefabsThatAllowUpgrades.Clear(); + prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs + .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) + .Select(it => it.Identifier)); } public bool CanBeApplied(Item item, UpgradePrefab? upgradePrefab) @@ -97,114 +143,146 @@ namespace Barotrauma if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } - return item.prefab.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); + return ((MapEntity)item).Prefab.GetAllowedUpgrades().Contains(Identifier) || + ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); } public bool CanBeApplied(XElement element, UpgradePrefab prefab) { - if (string.Equals("Structure", element.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { return IsWallUpgrade; } + if ("Structure" == element.NameAsIdentifier()) { return IsWallUpgrade; } - string identifier = element.GetAttributeString("identifier", string.Empty); - if (string.IsNullOrWhiteSpace(identifier)) { return false; } + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) { return false; } ItemPrefab? item = ItemPrefab.Find(null, identifier); if (item == null) { return false; } - string[] disallowedUpgrades = element.GetAttributeStringArray("disallowedupgrades", new string[0]); + Identifier[] disallowedUpgrades = element.GetAttributeIdentifierArray("disallowedupgrades", Array.Empty()); - if (disallowedUpgrades.Any(s => s.Equals(Identifier, StringComparison.OrdinalIgnoreCase) || s.Equals(prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return false; } + if (disallowedUpgrades.Any(s => s == Identifier || s == prefab.Identifier)) { return false; } return item.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); + ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier == tag); } - public static UpgradeCategory? Find(string idenfitier) + public static UpgradeCategory? Find(Identifier identifier) { - return !string.IsNullOrWhiteSpace(idenfitier) ? Categories.Find(category => string.Equals(category.Identifier, idenfitier, StringComparison.OrdinalIgnoreCase)) : null; + return !identifier.IsEmpty ? Categories.Find(category => category.Identifier == identifier) : null; } + + public override void Dispose() { } } - internal partial class UpgradePrefab : IPrefab, IDisposable + internal partial class UpgradePrefab : UpgradeContentPrefab { - public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public static readonly PrefabCollection Prefabs = new PrefabCollection( + onAdd: (prefab, isOverride) => + { + if (!prefab.SuppressWarnings && !isOverride) + { + foreach (UpgradePrefab matchingPrefab in Prefabs?.Where(p => p != prefab && p.TargetItems.Any(s => prefab.TargetItems.Contains(s))) ?? throw new NullReferenceException("Honestly I have no clue why this could be null...")) + { + if (matchingPrefab.isOverride) { continue; } + + var upgradePrefab = matchingPrefab.targetProperties; + string key = string.Empty; + + if (upgradePrefab.Keys.Any(s => prefab.targetProperties.Keys.Any(s1 => s == (key = s1)))) + { + if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => prefab.targetProperties[key].Contains(s))) + { + DebugConsole.AddWarning($"Upgrade \"{prefab.Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" + + "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" + + "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing."); + } + } + } + } + }, + onRemove: null, + onSort: null, + onAddOverrideFile: null, + onRemoveOverrideFile: null + ); public int MaxLevel { get; } - public string OriginalName { get; } - - public string Name { get; } - - public string Description { get; } + public LocalizedString Name { get; } + + public LocalizedString Description { get; } public float IncreaseOnTooltip { get; } - public string Identifier { get; } + private readonly ImmutableHashSet upgradeCategoryIdentifiers; - public string FilePath { get; } - - public UpgradeCategory[] UpgradeCategories { get; } + public IEnumerable UpgradeCategories + { + get + { + foreach (var id in upgradeCategoryIdentifiers) + { + if (UpgradeCategory.Categories.TryGet(id, out var category)) { yield return category!; } + } + } + } public UpgradePrice Price { get; } - public ContentPackage? ContentPackage { get; private set; } + private bool isOverride => Prefabs.IsOverride(this); - private bool IsOverride { get; } + public ContentXElement SourceElement { get; } - public XElement SourceElement { get; } - - private bool Disposed { get; set; } + private bool disposed; public bool SuppressWarnings { get; } public bool HideInMenus { get; } - public IEnumerable TargetItems => UpgradeCategories.SelectMany(u => u.ItemTags); + public IEnumerable TargetItems => UpgradeCategories.SelectMany(u => u.ItemTags); public bool IsWallUpgrade => UpgradeCategories.All(u => u.IsWallUpgrade); - private Dictionary TargetProperties { get; } + private Dictionary targetProperties { get; } - private UpgradePrefab(XElement element, string filePath, bool isOverride) + public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { - Name = element.GetAttributeString("name", string.Empty); - Description = element.GetAttributeString("description", string.Empty); + Name = element.GetAttributeString("name", string.Empty)!; + Description = element.GetAttributeString("description", string.Empty)!; MaxLevel = element.GetAttributeInt("maxlevel", 1); - Identifier = element.GetAttributeString("identifier", ""); SuppressWarnings = element.GetAttributeBool("supresswarnings", false); HideInMenus = element.GetAttributeBool("hideinmenus", false); - FilePath = filePath; SourceElement = element; - IsOverride = isOverride; - OriginalName = Name; var targetProperties = new Dictionary(); - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); - if (!string.IsNullOrWhiteSpace(nameIdentifier)) + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); + if (!nameIdentifier.IsEmpty) { - Name = TextManager.Get($"UpgradeName.{nameIdentifier}", returnNull: true) ?? string.Empty; + Name = TextManager.Get($"UpgradeName.{nameIdentifier}"); } - else if (string.IsNullOrWhiteSpace(Name)) + else if (Name.IsNullOrWhiteSpace()) { - Name = TextManager.Get($"UpgradeName.{Identifier}", returnNull: true) ?? string.Empty; + Name = TextManager.Get($"UpgradeName.{Identifier}"); } - string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); - if (!string.IsNullOrWhiteSpace(descriptionIdentifier)) + Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); + if (!descriptionIdentifier.IsEmpty) { - Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}", returnNull: true) ?? string.Empty; + Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}"); } - else if (string.IsNullOrWhiteSpace(Description)) + else if (Description.IsNullOrWhiteSpace()) { - Description = TextManager.Get($"UpgradeDescription.{Identifier}", returnNull: true) ?? string.Empty; + Description = TextManager.Get($"UpgradeDescription.{Identifier}"); } IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f); DebugConsole.Log(" " + Name); - foreach (XElement subElement in element.Elements()) +#if CLIENT + var decorativeSprites = new List(); +#endif + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -216,7 +294,7 @@ namespace Barotrauma #if CLIENT case "decorativesprite": { - DecorativeSprites.Add(new DecorativeSprite(subElement)); + decorativeSprites.Add(new DecorativeSprite(subElement)); break; } case "sprite": @@ -238,130 +316,24 @@ namespace Barotrauma } } - TargetProperties = targetProperties; +#if CLIENT + DecorativeSprites = decorativeSprites.ToImmutableArray(); +#endif - string[] categories = element.GetAttributeStringArray("categories", new string[] { }); - UpgradeCategories = (from category in UpgradeCategory.Categories from identifier in categories where string.Equals(category.Identifier, identifier) select category).ToArray(); + this.targetProperties = targetProperties; - if (!SuppressWarnings && !IsOverride) - { - foreach (UpgradePrefab matchingPrefab in Prefabs.Where(prefab => prefab.TargetItems.Any(s => TargetItems.Contains(s)))) - { - if (matchingPrefab.IsOverride) { continue; } - - var upgradePrefab = matchingPrefab.TargetProperties; - string key = string.Empty; - - if (upgradePrefab.Keys.Any(s => TargetProperties.Keys.Any(s1 => s == (key = s1)))) - { - if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => TargetProperties[key].Contains(s))) - { - DebugConsole.AddWarning($"Upgrade \"{Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" + - "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" + - "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing."); - } - } - } - } - - Prefabs.Add(this, isOverride); + upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty())? + .ToImmutableHashSet() ?? ImmutableHashSet.Empty; } public bool IsDisallowed(Item item) { - return item.disallowedUpgrades.Contains(Identifier) || UpgradeCategories.Any(c => item.disallowedUpgrades.Contains(c.Identifier)); + return item.DisallowedUpgradeSet.Contains(Identifier) || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier)); } - public static UpgradePrefab? Find(string identifier) + public static UpgradePrefab? Find(Identifier identifier) { - return !string.IsNullOrWhiteSpace(identifier) ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; - } - - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading upgrade module prefabs: "); - - foreach (ContentFile file in files) { LoadFromFile(file); } - } - - private static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - - var rootElement = doc?.Root; - if (rootElement == null) { return; } - - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "upgrademodule": - { - new UpgradePrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage }; - break; - } - case "upgradecategory": - { - new UpgradeCategory(rootElement); - break; - } - case "upgrademodules": - { - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var upgradeElement = element.GetChildElement("upgradeprefab"); - if (upgradeElement != null) - { - new UpgradePrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage }; - } - else - { - DebugConsole.ThrowError($"Cannot find an upgrade element from the children of the override element defined in {file.Path}"); - } - } - else - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "upgrademodule": - { - new UpgradePrefab(element, file.Path, false) { ContentPackage = file.ContentPackage }; - break; - } - case "upgradecategory": - { - new UpgradeCategory(element); - break; - } - } - } - } - - break; - } - case "override": - { - var upgrades = rootElement.GetChildElement("upgrademodules"); - if (upgrades != null) - { - foreach (var element in upgrades.Elements()) - { - new UpgradePrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - } - - foreach (var element in rootElement.GetChildElements("upgrademodule")) - { - new UpgradePrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - - break; - } - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " + - "Valid elements are: \"UpgradeModule\", \"UpgradeModules\" and \"Override\"."); - break; - } + return identifier != Identifier.Empty ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; } /// @@ -379,10 +351,10 @@ namespace Barotrauma /// ParsePercentage(element.GetAttributeString("increase", string.Empty)); /// /// - public static int ParsePercentage(string value, string? attribute = null, XElement? sourceElement = null, bool suppressWarnings = false) + public static int ParsePercentage(string value, Identifier attribute = default, XElement? sourceElement = null, bool suppressWarnings = false) { string? line = sourceElement?.ToString().Split('\n')[0].Trim(); - bool doWarnings = !suppressWarnings && attribute != null && sourceElement != null && line != null; + bool doWarnings = !suppressWarnings && !attribute.IsEmpty && sourceElement != null && line != null; if (string.IsNullOrWhiteSpace(value)) { @@ -426,25 +398,24 @@ namespace Barotrauma private void Dispose(bool disposing) { - if (!Disposed) + if (!disposed) { if (disposing) { Prefabs.Remove(this); #if CLIENT - Sprite.Remove(); + Sprite?.Remove(); Sprite = null; DecorativeSprites.ForEach(sprite => sprite.Remove()); - DecorativeSprites.Clear(); - TargetProperties.Clear(); + targetProperties.Clear(); #endif } } - Disposed = true; + disposed = true; } - public void Dispose() + public override void Dispose() { Dispose(true); GC.SuppressFinalize(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs new file mode 100644 index 000000000..98aaef668 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + public class CollectionConcat : ICollection + { + protected readonly IEnumerable enumerableA; + protected readonly IEnumerable enumerableB; + + public CollectionConcat(IEnumerable a, IEnumerable b) + { + enumerableA = a; enumerableB = b; + } + + public int Count => enumerableA.Count()+enumerableB.Count(); + + public bool IsReadOnly => true; + + public void Add(T item) => throw new InvalidOperationException(); + + public void Clear() => throw new InvalidOperationException(); + + public bool Remove(T item) => throw new InvalidOperationException(); + + public bool Contains(T item) => enumerableA.Contains(item) || enumerableB.Contains(item); + + public void CopyTo(T[] array, int arrayIndex) + { + void performCopy(IEnumerable enumerable) + { + if (enumerable is ICollection collection) + { + collection.CopyTo(array, arrayIndex); + arrayIndex += collection.Count; + } + else + { + foreach (var item in enumerable) + { + array[arrayIndex] = item; + arrayIndex++; + } + } + } + + performCopy(enumerableA); + performCopy(enumerableB); + } + + public IEnumerator GetEnumerator() + { + foreach (T item in enumerableA) { yield return item; } + foreach (T item in enumerableB) { yield return item; } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class ListConcat : CollectionConcat, IList, IReadOnlyList + { + public ListConcat(IEnumerable a, IEnumerable b) : base(a, b) { } + + public int IndexOf(T item) + { + int aCount = 0; + if (enumerableA is IList listA) + { + int index = listA.IndexOf(item); + if (index >= 0) { return index; } + aCount = listA.Count; + } + else + { + foreach (var a in enumerableA) + { + if (object.Equals(item, a)) { return aCount; } + aCount++; + } + } + + if (enumerableB is IList listB) + { + int index = listB.IndexOf(item); + if (index >= 0) { return index + aCount; } + } + else + { + foreach (var b in enumerableB) + { + if (object.Equals(item, b)) { return aCount; } + aCount++; + } + } + + return -1; + } + + public void Insert(int index, T item) + { + throw new InvalidOperationException(); + } + + public void RemoveAt(int index) + { + throw new InvalidOperationException(); + } + + public T this[int index] + { + get + { + int aCount = enumerableA.Count(); + return index < aCount ? enumerableA.ElementAt(index) : enumerableB.ElementAt(index - aCount); + } + set + { + throw new InvalidOperationException(); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CursedDictionary.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CursedDictionary.cs new file mode 100644 index 000000000..454d287ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CursedDictionary.cs @@ -0,0 +1,231 @@ +#if DEBUG && MODBREAKER +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + /// + /// Dictionary that's been deliberately designed to be a piece of + /// shit. Meant to be used to stress-test scenarios where we might + /// accidentally be relying on the implementation details of a + /// dictionary that shouldn't be relied on. + /// + public class CursedDictionary : IDictionary, IReadOnlyDictionary where TKey: notnull + { + private ICollection keys; + private ICollection values; + private readonly List> keyValuePairs = new List>(); + private readonly Dictionary keyToKvpIndex = new Dictionary(); + private readonly object mutex = new object(); + + private readonly Random rng; + + public CursedDictionary() + { + rng = new Random((int)(DateTime.Now.ToBinary() % int.MaxValue)); + keys = Array.Empty(); + values = Array.Empty(); + } + + private void Refresh() + { + keys = keyValuePairs.Select(kvp => kvp.Key).ToArray(); + values = keyValuePairs.Select(kvp => kvp.Value).ToArray(); + keyToKvpIndex.Clear(); + for (int i=0; i> GetEnumerator() + { + KeyValuePair[] clone; + lock (mutex) + { + keyValuePairs.Shuffle(rng); + Refresh(); + clone = keyValuePairs.ToArray(); + } + + foreach (var kvp in clone) + { + yield return kvp; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(KeyValuePair item) + { + lock (mutex) + { + if (keyToKvpIndex.ContainsKey(item.Key)) + { + throw new InvalidOperationException($"Duplicate key: {item.Key}"); + } + + keyValuePairs.Add(item); + keyValuePairs.Shuffle(rng); + Refresh(); + } + } + + public void Clear() + { + lock (mutex) + { + keyValuePairs.Clear(); + Refresh(); + } + } + + public bool Contains(KeyValuePair item) + { + lock (mutex) + { + return keyValuePairs.Contains(item); + } + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + lock (mutex) + { + keyValuePairs.Shuffle(rng); + keyValuePairs.CopyTo(array, arrayIndex); + Refresh(); + } + } + + public bool Remove(KeyValuePair item) + { + lock (mutex) + { + bool success = keyValuePairs.Remove(item); + keyValuePairs.Shuffle(rng); + Refresh(); + return success; + } + } + + public int Count + { + get + { + lock (mutex) + { + return keyValuePairs.Count; + } + } + } + + public bool IsReadOnly => false; + + public void Add(TKey key, TValue value) => Add(new KeyValuePair(key, value)); + + public bool ContainsKey(TKey key) + { + lock (mutex) + { + return keyToKvpIndex.ContainsKey(key); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (mutex) + { + value = default!; + bool success = keyToKvpIndex.TryGetValue(key, out int index); + if (success) + { + value = keyValuePairs[index].Value; + } + + keyValuePairs.Shuffle(rng); + Refresh(); + return success; + } + } + + public bool Remove(TKey key) => TryRemove(key, out _); + + public bool TryRemove(TKey key, out TValue value) + { + lock (mutex) + { + value = default!; + bool success = false; + if (keyToKvpIndex.TryGetValue(key, out int index)) + { + value = keyValuePairs[index].Value; + keyValuePairs.RemoveAt(index); + success = true; + } + + keyValuePairs.Shuffle(rng); + Refresh(); + return success; + } + } + + public TValue this[TKey key] + { + get + { + lock (mutex) + { + return keyValuePairs[keyToKvpIndex[key]].Value; + } + } + set + { + lock (mutex) + { + if (!keyToKvpIndex.ContainsKey(key)) + { + Add(key, value); + } + else + { + keyValuePairs[keyToKvpIndex[key]] = new KeyValuePair(key, value); + } + + keyValuePairs.Shuffle(rng); + Refresh(); + } + } + } + + + public ICollection Keys + { + get + { + lock (mutex) + { + return keys.ToArray(); + } + } + } + public ICollection Values + { + get + { + lock (mutex) + { + return values.ToArray(); + } + } + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + IEnumerable IReadOnlyDictionary.Values => Values; + } +} +#endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs index 30ce32a0c..b7c975946 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs @@ -7,7 +7,7 @@ namespace Barotrauma class Homoglyphs { ///List of homoglyphs taken from https://github.com/codebox/homoglyph/ - private static List homoglyphs = new List(){ + private readonly static uint[][] homoglyphs = { new uint[]{0x20,0xa0,0x1680,0x2000,0x2001,0x2002,0x2003,0x2004,0x2005,0x2006,0x2007,0x2008,0x2009,0x200a,0x2028,0x2029,0x202f,0x205f}, new uint[]{0x21,0x1c3,0x2d51,0xff01}, new uint[]{0x24,0xff04}, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs index 1ec3d25a0..04c00952d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs @@ -35,9 +35,9 @@ namespace Barotrauma { public sealed class HttpUtility { - public static Dictionary ParseQueryString(string query) + public static Dictionary ParseQueryString(string query) { - Dictionary collection = new Dictionary(); + Dictionary collection = new Dictionary(); var splitGet = query.Split('?'); if (splitGet.Length > 1) { @@ -47,7 +47,7 @@ namespace Barotrauma var splitKeyValue = kvp.Split('='); if (splitKeyValue.Length > 1) { - collection.Add(splitKeyValue[0], splitKeyValue[1]); + collection.Add(splitKeyValue[0].ToIdentifier(), splitKeyValue[1]); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index 81af9005f..a9cebf7c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -22,12 +22,12 @@ namespace Barotrauma if (parentElement is { HasElements: true }) { srcRanges = new List>(); - foreach (XElement subElement in parentElement.Elements()) + foreach (var subElement in parentElement.Elements()) { int id = subElement.GetAttributeInt("ID", -1); if (id > 0) { InsertId(id); } } - maxId = GetOffsetId(srcRanges.Last().End); + maxId = GetOffsetId(srcRanges.Any() ? srcRanges.Last().End : offset); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/LinkedPairSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/LinkedPairSet.cs new file mode 100644 index 000000000..7dead65e4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/LinkedPairSet.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Barotrauma +{ + public class LinkedPairSet : IEnumerable<(T1, T2)> + { + private readonly Dictionary t1ToT2 = new Dictionary(); + private readonly Dictionary t2ToT1 = new Dictionary(); + + public bool Contains(T1 t1) + { + return t1ToT2.ContainsKey(t1); + } + + public bool Contains(T2 t2) + { + return t2ToT1.ContainsKey(t2); + } + + public T2 this[T1 t1] + { + get { return t1ToT2[t1]; } + set + { + T2 prevT2 = t1ToT2[t1]; + t2ToT1.Remove(prevT2); t2ToT1.Add(value, t1); + t1ToT2[t1] = value; + } + } + + public T1 this[T2 t2] + { + get { return t2ToT1[t2]; } + set + { + T1 prevT1 = t2ToT1[t2]; + t1ToT2.Remove(prevT1); t1ToT2.Add(value, t2); + t2ToT1[t2] = value; + } + } + + public void Add(T1 t1, T2 t2) + { + if (Contains(t1)) { throw new ArgumentException($"{GetType().Name} already contains {t1}"); } + if (Contains(t2)) { throw new ArgumentException($"{GetType().Name} already contains {t2}"); } + t1ToT2.Add(t1, t2); + t2ToT1.Add(t2, t1); + } + + public void Remove(T1 t1) + { + T2 t2 = t1ToT2[t1]; + t1ToT2.Remove(t1); + t2ToT1.Remove(t2); + } + + public void Remove(T2 t2) + { + T1 t1 = t2ToT1[t2]; + t1ToT2.Remove(t1); + t2ToT1.Remove(t2); + } + + public IEnumerator<(T1, T2)> GetEnumerator() + { + foreach (var t1 in t1ToT2.Keys) + { + yield return (t1, t1ToT2[t1]); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ListDictionary.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ListDictionary.cs new file mode 100644 index 000000000..21cdf1480 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ListDictionary.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class ListDictionary : IReadOnlyDictionary + { + private readonly ImmutableDictionary keyToIndex; + private readonly IReadOnlyList list; + + public ListDictionary(IReadOnlyList list, int len, Func keyFunc) + { + this.list = list; + var keyToIndex = new Dictionary(); + for (int i = 0; i < len; i++) + { + keyToIndex.Add(keyFunc(i), i); + } + + this.keyToIndex = keyToIndex.ToImmutableDictionary(); + } + + public IEnumerator> GetEnumerator() + { + foreach (var kvp in keyToIndex) + { + yield return new KeyValuePair(kvp.Key, list[kvp.Value]); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => keyToIndex.Count; + public bool ContainsKey(TKey key) => keyToIndex.ContainsKey(key); + + public bool TryGetValue(TKey key, out TValue value) + { + if (keyToIndex.TryGetValue(key, out int index)) + { + value = list[index]; + return true; + } + value = default(TValue); + return false; + } + + public TValue this[TKey key] => list[keyToIndex[key]]; + + public IEnumerable Keys => keyToIndex.Keys; + public IEnumerable Values => keyToIndex.Values.Select(i => list[i]); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 77ddd1d2f..688c284c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -749,7 +749,7 @@ namespace Barotrauma Vector2 normal = Vector2.Normalize(endSegment - startSegment); normal = new Vector2(-normal.Y, normal.X); - midPoint += normal * Rand.Range(-offsetAmount, offsetAmount, Rand.RandSync.Server); + midPoint += normal * Rand.Range(-offsetAmount, offsetAmount, Rand.RandSync.ServerAndClient); if (bounds.HasValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs new file mode 100644 index 000000000..08631f611 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs @@ -0,0 +1,9 @@ +namespace Barotrauma +{ + public sealed class None : Option + { + private None() { } + + public static Option Create() => new None(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs new file mode 100644 index 000000000..1bdc94160 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -0,0 +1,14 @@ +namespace Barotrauma +{ + /// + /// Implementation of Option type. + /// + /// + /// Credit Jlobblet + /// + public abstract class Option + { + public static Option Some(T value) => Some.Create(value); + public static Option None() => None.Create(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs new file mode 100644 index 000000000..fad94a2a7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs @@ -0,0 +1,17 @@ +using System; + +namespace Barotrauma +{ + public sealed class Some : Option + { + public readonly T Value; + + private Some(T value) + { + if (value is null) { throw new ArgumentNullException(nameof(value), "Some cannot contain null"); } + Value = value; + } + + public static Option Create(T value) => new Some(value); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index 7f0193f6e..55be14eb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -1,26 +1,68 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Barotrauma.IO; using Voronoi2; namespace Barotrauma { public static class Rand { + [Obsolete("TODO: remove")] + public static class Tracker + { + private readonly static List logMsgs = new List(); + public static IReadOnlyList LogMsgs => logMsgs; + + public static bool Active = false; + + public static void Reset() + { + logMsgs.Clear(); + Active = false; + } + + public static void RegisterCall(int stDepth=4) + { + if (!Active) { return; } + var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true); + var frames = st.GetFrames(); + string msg = string.Join("; ", + frames.Take(stDepth).Select(f => + $"{Path.GetFileNameWithoutExtension(f.GetFileName())}:{f.GetFileLineNumber()}")); + logMsgs.Add(msg); + } + + public static void Log(string msg) + { + if (!Active) { return; } + logMsgs.Add(msg); + } + } + public enum RandSync { - Unsynced = -1, //not synced, used for unimportant details like minor particle properties - Server = 0, //synced with the server (used for gameplay elements that the players can interact with) - ClientOnly = 1 //set to match between clients (used for misc elements that the server doesn't track, but clients want to match anyway) + Unsynced, //not synced, used for unimportant details like minor particle properties + ServerAndClient, //synced with the server (used for gameplay elements that the players can interact with) +#if CLIENT + ClientOnly //set to match between clients (used for misc elements that the server doesn't track, but clients want to match anyway) +#endif } private static Random localRandom = new Random(); - private static readonly Random[] syncedRandom = new MTRandom[] { - new MTRandom(), new MTRandom() + private static readonly Dictionary syncedRandom = new Dictionary { + { RandSync.ServerAndClient, new MTRandom() }, +#if CLIENT + { RandSync.ClientOnly, new MTRandom() } +#endif }; public static Random GetRNG(RandSync randSync) { - return randSync == RandSync.Unsynced ? localRandom : syncedRandom[(int)randSync]; + return randSync == RandSync.Unsynced ? localRandom : syncedRandom[randSync]; } public static void SetLocalRandom(int seed) @@ -30,21 +72,32 @@ namespace Barotrauma public static void SetSyncedSeed(int seed) { - syncedRandom[(int)RandSync.Server] = new MTRandom(seed); - syncedRandom[(int)RandSync.ClientOnly] = new MTRandom(seed); + syncedRandom[RandSync.ServerAndClient] = new MTRandom(seed); +#if CLIENT + syncedRandom[RandSync.ClientOnly] = new MTRandom(seed); +#endif } public static int ThreadId = 0; private static void CheckRandThreadSafety(RandSync sync) { - if (ThreadId != 0 && sync == RandSync.Server) + if (sync == RandSync.ServerAndClient) { Tracker.RegisterCall(); } + + if (ThreadId != 0 && sync == RandSync.Unsynced) + { + if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId) + { + Debug.WriteLine($"Unsynced rand used in synced thread! {Environment.StackTrace}"); + } + } + if (ThreadId != 0 && sync == RandSync.ServerAndClient) { if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId) { #if DEBUG - throw new Exception("Unauthorized multithreaded access to RandSync.Server"); + throw new Exception("Unauthorized multithreaded access to RandSync.ServerAndClient"); #else - DebugConsole.ThrowError("Unauthorized multithreaded access to RandSync.Server\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError("Unauthorized multithreaded access to RandSync.ServerAndClient\n" + Environment.StackTrace.CleanupStackTrace()); #endif } } @@ -53,13 +106,13 @@ namespace Barotrauma public static float Range(float minimum, float maximum, RandSync sync=RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (float)(sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).NextDouble() * (maximum - minimum) + minimum; + return (float)(sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum; } public static double Range(double minimum, double maximum, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).NextDouble() * (maximum - minimum) + minimum; + return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum; } /// @@ -68,13 +121,13 @@ namespace Barotrauma public static int Range(int minimum, int maximum, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).Next(maximum - minimum) + minimum; + return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).Next(maximum - minimum) + minimum; } public static int Int(int max, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).Next(max); + return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).Next(max); } public static Vector2 Vector(float length, RandSync sync = RandSync.Unsynced) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs index 51d66c2b4..54c09d18e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs @@ -7,10 +7,13 @@ namespace Barotrauma public static class ReadOnlyListExtensions { public static int IndexOf(this IReadOnlyList list, T elem) + => list.IndexOf(input => input.Equals(elem)); + + public static int IndexOf(this IReadOnlyList list, Func predicate) { for (int i = 0; i < list.Count; i++) { - if (list[i].Equals(elem)) { return i; } + if (predicate(list[i])) { return i; } } return -1; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs new file mode 100644 index 000000000..2a1585e5b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Barotrauma +{ + public static class ReflectionUtils + { + public static IEnumerable GetDerivedNonAbstract() + { + return Assembly.GetEntryAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs new file mode 100644 index 000000000..71de5e5a3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -0,0 +1,43 @@ +#nullable enable +namespace Barotrauma +{ + public abstract class Result + where T: notnull + where TError: notnull + { + public abstract bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + + public static Success Success(T value) + => new Success(value); + + public static Failure Failure(TError error) + => new Failure(error); + } + + public sealed class Success : Result + where T: notnull + where TError: notnull + { + public readonly T Value; + public override bool IsSuccess => true; + + public Success(T value) + { + Value = value; + } + } + + public sealed class Failure : Result + where T: notnull + where TError: notnull + { + public readonly TError Error; + public override bool IsSuccess => false; + + public Failure(TError error) + { + Error = error; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs index 2493257e4..47445df31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { @@ -20,18 +21,17 @@ namespace Barotrauma private const string metadataDefinition = "metadata"; private const string endDefinition = "end"; - public static List GetRichTextData(string text, out string sanitizedText) + public static ImmutableArray? GetRichTextData(string text, out string sanitizedText) { - List textColors = null; sanitizedText = text; - if (!string.IsNullOrEmpty(text) && text.Contains(definitionIndicator)) + if (!string.IsNullOrEmpty(text) && text.Contains(definitionIndicator, System.StringComparison.Ordinal)) { text = text.Replace("\r", ""); string[] segments = text.Split(definitionIndicator); sanitizedText = string.Empty; - textColors = new List(); + List textColors = new List(); RichTextData tempData = null; int prevIndex = 0; @@ -80,9 +80,9 @@ namespace Barotrauma } } } + return textColors.ToImmutableArray(); } - - return textColors; + return null; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 2fece0b4d..fefee9d61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +8,7 @@ namespace Barotrauma.IO { static class Validation { - private static readonly string[] unwritableDirs = new string[] { "Content", "Data/ContentPackages" }; + private static readonly string[] unwritableDirs = new string[] { "Content" }; private static readonly string[] unwritableExtensions = new string[] { ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe and .dll handled separately in CanWrite) @@ -58,7 +60,11 @@ namespace Barotrauma.IO public static class SafeXML { - public static void SaveSafe(this System.Xml.Linq.XDocument doc, string path, bool throwExceptions = false) + public static void SaveSafe( + this System.Xml.Linq.XDocument doc, + string path, + System.Xml.Linq.SaveOptions saveOptions = System.Xml.Linq.SaveOptions.None, + bool throwExceptions = false) { if (!Validation.CanWrite(path, false)) { @@ -73,7 +79,7 @@ namespace Barotrauma.IO } return; } - doc.Save(path); + doc.Save(path, saveOptions); } public static void SaveSafe(this System.Xml.Linq.XElement element, string path, bool throwExceptions = false) @@ -107,7 +113,7 @@ namespace Barotrauma.IO public class XmlWriter : IDisposable { - public readonly System.Xml.XmlWriter Writer; + public readonly System.Xml.XmlWriter? Writer; public XmlWriter(string path, System.Xml.XmlWriterSettings settings) { @@ -156,65 +162,42 @@ namespace Barotrauma.IO } } + public static class XmlWriterExtensions + { + public static void Save(this System.Xml.Linq.XDocument doc, XmlWriter writer) + { + doc.Save(writer.Writer ?? throw new NullReferenceException("Unable to save XML document: XML writer is null.")); + } + } + public static class Path { public static readonly char DirectorySeparatorChar = System.IO.Path.DirectorySeparatorChar; public static readonly char AltDirectorySeparatorChar = System.IO.Path.AltDirectorySeparatorChar; - public static string GetExtension(string path) - { - return System.IO.Path.GetExtension(path); - } + public static string GetExtension(string path) => System.IO.Path.GetExtension(path); - public static string GetFileNameWithoutExtension(string path) - { - return System.IO.Path.GetFileNameWithoutExtension(path); - } + public static string GetFileNameWithoutExtension(string path) => System.IO.Path.GetFileNameWithoutExtension(path); - public static string GetPathRoot(string path) - { - return System.IO.Path.GetPathRoot(path); - } + public static string? GetPathRoot(string? path) => System.IO.Path.GetPathRoot(path); - public static string GetRelativePath(string relativeTo, string path) - { - return System.IO.Path.GetRelativePath(relativeTo, path); - } + public static string GetRelativePath(string relativeTo, string path) => System.IO.Path.GetRelativePath(relativeTo, path); - public static string GetDirectoryName(string path) - { - return System.IO.Path.GetDirectoryName(path); - } + public static string GetDirectoryName(ContentPath path) => GetDirectoryName(path.Value)!; + + public static string? GetDirectoryName(string path) => System.IO.Path.GetDirectoryName(path); - public static string GetFileName(string path) - { - return System.IO.Path.GetFileName(path); - } + public static string GetFileName(string path) => System.IO.Path.GetFileName(path); - public static string GetFullPath(string path) - { - return System.IO.Path.GetFullPath(path); - } + public static string GetFullPath(string path) => System.IO.Path.GetFullPath(path); - public static string Combine(params string[] s) - { - return System.IO.Path.Combine(s); - } + public static string Combine(params string[] s) => System.IO.Path.Combine(s); - public static string GetTempFileName() - { - return System.IO.Path.GetTempFileName(); - } + public static string GetTempFileName() => System.IO.Path.GetTempFileName(); - public static bool IsPathRooted(string path) - { - return System.IO.Path.IsPathRooted(path); - } - public static IEnumerable GetInvalidFileNameChars() - { - return System.IO.Path.GetInvalidFileNameChars(); - } + public static bool IsPathRooted(string path) => System.IO.Path.IsPathRooted(path); + public static IEnumerable GetInvalidFileNameChars() => System.IO.Path.GetInvalidFileNameChars(); } public static class Directory @@ -229,22 +212,22 @@ namespace Barotrauma.IO System.IO.Directory.SetCurrentDirectory(path); } - public static IEnumerable GetFiles(string path) + public static string[] GetFiles(string path) { return System.IO.Directory.GetFiles(path); } - public static IEnumerable GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories) + public static string[] GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories) { return System.IO.Directory.GetFiles(path, pattern, option); } - public static IEnumerable GetDirectories(string path) + public static string[] GetDirectories(string path, string searchPattern = "*", System.IO.SearchOption searchOption = System.IO.SearchOption.TopDirectoryOnly) { - return System.IO.Directory.GetDirectories(path); + return System.IO.Directory.GetDirectories(path, searchPattern, searchOption); } - public static IEnumerable GetFileSystemEntries(string path) + public static string[] GetFileSystemEntries(string path) { return System.IO.Directory.GetFileSystemEntries(path); } @@ -264,7 +247,7 @@ namespace Barotrauma.IO return System.IO.Directory.Exists(path); } - public static System.IO.DirectoryInfo CreateDirectory(string path) + public static System.IO.DirectoryInfo? CreateDirectory(string path) { if (!Validation.CanWrite(path, true)) { @@ -289,10 +272,9 @@ namespace Barotrauma.IO public static class File { - public static bool Exists(string path) - { - return System.IO.File.Exists(path); - } + public static bool Exists(ContentPath path) => Exists(path.Value); + + public static bool Exists(string path) => System.IO.File.Exists(path); public static void Copy(string src, string dest, bool overwrite=false) { @@ -319,6 +301,8 @@ namespace Barotrauma.IO System.IO.File.Move(src, dest); } + public static void Delete(ContentPath path) => Delete(path.Value); + public static void Delete(string path) { if (!Validation.CanWrite(path, false)) @@ -334,7 +318,7 @@ namespace Barotrauma.IO return System.IO.File.GetLastWriteTime(path); } - public static FileStream Open( + public static FileStream? Open( string path, System.IO.FileMode mode, System.IO.FileAccess access = System.IO.FileAccess.ReadWrite, @@ -362,17 +346,17 @@ namespace Barotrauma.IO return new FileStream(path, System.IO.File.Open(path, mode, access, shareVal)); } - public static FileStream OpenRead(string path) + public static FileStream? OpenRead(string path) { return Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read); } - public static FileStream OpenWrite(string path) + public static FileStream? OpenWrite(string path) { return Open(path, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write); } - public static FileStream Create(string path) + public static FileStream? Create(string path) { return Open(path, System.IO.FileMode.Create, System.IO.FileAccess.Write); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 6e427d5ce..4101d9513 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -150,7 +150,7 @@ namespace Barotrauma if (ownedSubsElement != null) { ownedSubmarines = new List(); - foreach (XElement subElement in ownedSubsElement.Elements()) + foreach (var subElement in ownedSubsElement.Elements()) { string subName = subElement.GetAttributeString("name", ""); string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); @@ -271,7 +271,7 @@ namespace Barotrauma string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; if (fileName == "Save_Default") { - fileName = TextManager.Get("SaveFile.DefaultName", true); + fileName = TextManager.Get("SaveFile.DefaultName").Value; if (fileName.Length == 0) fileName = "Save"; } @@ -381,7 +381,7 @@ namespace Barotrauma char c = BitConverter.ToChar(bytes, 0); sb.Append(c); } - string sFileName = sb.ToString(); + string sFileName = sb.ToString().Replace('\\', '/'); fileName = sFileName; progress?.Invoke(sFileName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index bc5aa7d5b..e165d24e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -122,7 +122,7 @@ namespace Barotrauma enumPath = string.IsNullOrWhiteSpace(startPath) ? "./" : startPath; } - List filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToList(); + string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray(); if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal))) { @@ -130,7 +130,7 @@ namespace Barotrauma } else { - List correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToList(); + string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray(); if (correctedPaths.Any()) { corrected = true; @@ -159,7 +159,7 @@ namespace Barotrauma return fileName; } - private static System.Text.RegularExpressions.Regex removeBBCodeRegex = + private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex = new System.Text.RegularExpressions.Regex(@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]"); public static string RemoveBBCodeTags(string str) @@ -168,15 +168,6 @@ namespace Barotrauma return removeBBCodeRegex.Replace(str, ""); } - public static string LimitString(string str, int maxCharacters) - { - if (str == null || maxCharacters < 0) return null; - - if (maxCharacters < 4 || str.Length <= maxCharacters) return str; - - return str.Substring(0, maxCharacters - 3) + "..."; - } - public static string RandomSeed(int length) { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -186,6 +177,8 @@ namespace Barotrauma .ToArray()); } + public static int IdentifierToInt(Identifier id) => StringToInt(id.Value.ToLowerInvariant()); + public static int StringToInt(string str) { str = str.Substring(0, Math.Min(str.Length, 32)); @@ -201,6 +194,7 @@ namespace Barotrauma return BitConverter.ToInt32(asciiBytes, 0); } + /// /// a method for changing inputtypes with old names to the new ones to ensure backwards compatibility with older subs /// @@ -264,16 +258,17 @@ namespace Barotrauma { string color = obj switch { - bool b => b ? "80,250,123" : "255,85,85", - string _ => "241,250,140", - int _ => "189,147,249", - float _ => "189,147,249", - double _ => "189,147,249", - null => "255,85,85", + bool b => b ? "80,250,123" : "255,85,85", + string _ => "241,250,140", + Identifier _ => "241,250,140", + int _ => "189,147,249", + float _ => "189,147,249", + double _ => "189,147,249", + null => "255,85,85", _ => "139,233,253" }; - return obj is string + return obj is string || obj is Identifier ? $"‖color:{color}‖\"{obj}\"‖color:end‖" : $"‖color:{color}‖{obj ?? "null"}‖color:end‖"; } @@ -352,7 +347,7 @@ namespace Barotrauma return d[n, m]; } - public static string SecondsToReadableTime(float seconds) + public static LocalizedString SecondsToReadableTime(float seconds) { int s = (int)(seconds % 60.0f); if (seconds < 60.0f) @@ -363,23 +358,23 @@ namespace Barotrauma int h = (int)(seconds / (60.0f * 60.0f)); int m = (int)((seconds / 60.0f) % 60); - string text = ""; + LocalizedString text = ""; if (h != 0) { text = TextManager.GetWithVariable("timeformathours", "[hours]", h.ToString()); } if (m != 0) { - string minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString()); - text = string.IsNullOrEmpty(text) ? minutesText : string.Join(" ", text, minutesText); + LocalizedString minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString()); + text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(" ", text, minutesText); } if (s != 0) { - string secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString()); - text = string.IsNullOrEmpty(text) ? secondsText : string.Join(" ", text, secondsText); + LocalizedString secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString()); + text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(" ", text, secondsText); } return text; } private static Dictionary> cachedLines = new Dictionary>(); - public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.Server) + public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { List lines; if (cachedLines.ContainsKey(filePath)) @@ -426,7 +421,20 @@ namespace Barotrauma return buffer; } - public static T SelectWeightedRandom(IList objects, IList weights, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Rand.RandSync randSync) + { + return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync)); + } + + + public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) + { + List objectList = objects.ToList(); + List weights = objectList.Select(o => weightMethod(o)).ToList(); + return SelectWeightedRandom(objectList, weights, random); + } + + public static T SelectWeightedRandom(IList objects, IList weights, Rand.RandSync randSync) { return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync)); } @@ -455,11 +463,14 @@ namespace Barotrauma return default(T); } + public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5) + => StringToUInt32Hash(id.Value.ToLowerInvariant(), md5); + public static UInt32 StringToUInt32Hash(string str, MD5 md5) { //calculate key based on MD5 hash instead of string.GetHashCode //to ensure consistent results across platforms - byte[] inputBytes = Encoding.ASCII.GetBytes(str); + byte[] inputBytes = Encoding.UTF8.GetBytes(str); byte[] hash = md5.ComputeHash(inputBytes); UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead? @@ -609,16 +620,6 @@ namespace Barotrauma return commands.ToArray(); } - public static void OpenFileWithShell(string filename) - { - ProcessStartInfo startInfo = new ProcessStartInfo() - { - FileName = filename, - UseShellExecute = true - }; - Process.Start(startInfo); - } - /// /// Cleans up a path by replacing backslashes with forward slashes, and /// optionally corrects the casing of the path. Recommended when serializing diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub deleted file mode 100644 index 26e46ca4f..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub deleted file mode 100644 index 1bb604f3e..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub deleted file mode 100644 index b87771309..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub deleted file mode 100644 index 930f93d70..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub deleted file mode 100644 index 13e491cf7..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Herja.sub b/Barotrauma/BarotraumaShared/Submarines/Herja.sub deleted file mode 100644 index 246bbcdcd..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Herja.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub deleted file mode 100644 index 86acde003..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub deleted file mode 100644 index db4d8efad..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub deleted file mode 100644 index 0696aa7ab..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub deleted file mode 100644 index a770b9e08..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub deleted file mode 100644 index 0cf0024d3..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/PowerTestSub.sub b/Barotrauma/BarotraumaShared/Submarines/PowerTestSub.sub new file mode 100644 index 000000000..ea439c4cb Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/PowerTestSub.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub deleted file mode 100644 index dbe786c4b..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub deleted file mode 100644 index 0fed8bcee..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub deleted file mode 100644 index 6361caafd..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub deleted file mode 100644 index cec49a349..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub deleted file mode 100644 index 0771f67c3..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub deleted file mode 100644 index 9f602a6e0..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub deleted file mode 100644 index f11f731da..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub b/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub deleted file mode 100644 index 87b70f6e6..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 2002433de..e863904a0 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,76 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.0.0 +--------------------------------------------------------------------------------------------------------- + +Modding: +- An extensive rewrite of how the game handles content packages and loading content. Addresses a ton of issues, inconsistencies and usability issues regarding modding. +- The Mods folder has been replaced by folders called "WorkshopMods" and "LocalMods". "WorkshopMods" is used to store mods installed from the Workshop, and any mods stored in it should never need to be modified manually. "LocalMods" is used for developing mods: installing/updating mods never modifies the contents of this folder to prevent any work from being lost. +- Submarines are no longer saved in the Submarines folder, because it made it easy to get vanilla and custom subs mixed up. The sub editor now automatically creates a new local mod for each saved submarine. +- Remade the Workshop menu and made it a tab of the settings menu. +- More robust handling of mod load order, overrides and variants. +- Clients now download the mods a server is using directly from the server, fixing content mismatches when for example trying to join a server that uses outdated mods. +- Mods now have version numbers to make it easier to determine which of the players are out of date. +- The Workshop preview images are no longer saved into the game folder, fixing the folder gradually growing in size as you use browse mods in the Workshop menu. +- Switching languages no longer requires restarting the game. +- Music can be overridden by identifiers. +- Character gender and ethnicity are no longer hard-coded: modders can use any kind of arbitrary tags to categorize character sprites.- Option to set the condition of an item spawned with status effects. +- Added new "accessiblebyowner" property to inventories. Allows making a character able to access their inventory even when "accessiblewhenalive" is false. + +Changes and additions: +- Reworked the power distribution logic. Should fix unstability and inconsistencies in power grids involving relays and batteries. +- Ballast flora improvements: improved damage visuals, branches die when cut from the root, the flora regenerates health at a rate relative to it's size. +- Made text displays and terminals craftable/attachable/detachable. +- Made Concat component's separator field editable mid-round. +- Made Deadeye Carbine fire in bursts. +- Characters spawn at a spawnpoint appopriate for their job when using the console command "spawn [job] inside". +- Added "low_oxygen" output to oxygen detectors. +- Beacon missions can spawn other types of monsters than just crawlers. + +Balance: +- Increased the price of calyxanide to make it more in line with the price of husk eggs. +- Fixed wrecked supply cabinets being treated as normal supply cabinets. Reduces the amount of loot spawned in them. +- Adjust the medical item spawns in wreck and abandoned med cabinets: less powerful medicines. Basic ingredients and consumables are more common, but come in low quantities. +- Increased the amount of scrap in wrecks. +- Increased Gene Splicer's price from 200 to 500. +- Increase the commonness of Esperite and Galena, which are sources of lead. + +AI: +- Fixed bots accidentally shooting with a weapon if they have it equipped when they try to use the underwater scooter. +- Fixed bots still sometimes getting stuck when trying to get something from or put something to secure lockers. +- Fixed bots acting weird while trying to use underwater scooters inside. +- Fixed bots failing to heal characters in a docked sub/shuttle. + +Talents: +- Fix to yet another issue that sometimes prevented unlocking additional talents after unlocking "All-seeing Eye". +- Fixed "Curiosity" talent not giving experience to allies. +- Removing or changing order priority doesn't trigger the "Commander" talent buff. +- Fixed "Mass Production" talent allowing you to power devices by tinkering. +- Fixed "Pyromaniac" giving 39.9% damage resistance instead of 40%. +- Fixed ability to tinker indefinitely by interrupting the tinkering by switching to repairing the item. + +Fixes: +- Fixed monsters being able to attack with practically no cooldown when they're taking constant damage from a player. +- Fixed crawler eggs not being displayed on the sonar. +- Fixed aiming being slightly off when crouching and not moving. +- Fixed item interfaces getting misaligned when there's several linked item UIs visible at the same time. +- Fixed inability to interact with doors/hatches through docking ports. +- Fixed escort missions failing if the escorted characters are in a shuttle docked to the main sub. +- Fixed pumps rounding the pumping rate value to -39 instead of -40. +- Fixed shell shields dropping from moloch's inventories when they spawn (because they could only be placed in hand slots which the molochs don't have). +- Fixed linked subs not being taken into account in the cargo capacity displayed in the sub's info panel. +- Fixed ID card descriptions disappearing between rounds. +- Fixed ID card description not being added if the ID card tags are empty. +- Fixed "error" text in the medical clinic UI when trying to heal a character who's disconnected. +- Fixed texts overlapping on health scanner hud when using a text scale above 100%. +- Fixed "snap to grid" not working on structures. +- Update items' hulls after creating the hulls between docking ports. Fixes e.g. water detectors between docking ports still thinking they're outside after the ports lock and the area between them drains. +- Fixed decorative sprites rotating incorrectly on vertically mirrored items. +- Disable collisions between subs when "locking" the docking ports between them. Fixes the ports leaving a gap between them if some structures of the sub prevent them from aligning exactly. +- Fixed wifi components ignoring signals to the "signal_in" connection if the signal originated from another wifi component that can't transmit to this one (i.e. if a wifi component passes a signal through a wire to another wifi component that uses a different channel). +- Fixed dropped signal components being drawn behind devices (now they're only drawn behind devices when attached to a wall). +- Fixed SMGs autofilled into the subs sometimes spawning without magazines. +- Fixed misaligned "Label Number 6" decal. + --------------------------------------------------------------------------------------------------------- v0.16.7.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/config.xml b/Barotrauma/BarotraumaShared/config.xml deleted file mode 100644 index 8e811973a..000000000 --- a/Barotrauma/BarotraumaShared/config.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - diff --git a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs index ad7700d48..680c54238 100644 --- a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs +++ b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -221,7 +222,7 @@ namespace Steamworks } // Remove it before we do anything, incase the continuation throws exceptions - ResultCallbacks.Remove( result.AsyncCall ); + ResultCallbacks.Remove( result.AsyncCall, out _ ); // At this point whatever async routine called this // continues running. @@ -262,7 +263,7 @@ namespace Steamworks public bool server; } - static Dictionary ResultCallbacks = new Dictionary(); + static ConcurrentDictionary ResultCallbacks = new ConcurrentDictionary(); /// /// Watch for a steam api call @@ -305,6 +306,15 @@ namespace Steamworks } ); } + private static void RemoveCallbacks(Func, bool> predicate) + { + var toRemove = ResultCallbacks.Where( predicate ).Select(kvp => kvp.Key).ToArray(); + foreach (var key in toRemove) + { + ResultCallbacks.Remove(key, out _); + } + } + internal static void ShutdownServer() { ServerPipe = 0; @@ -314,8 +324,7 @@ namespace Steamworks Callbacks[callback.Key].RemoveAll( x => x.server ); } - ResultCallbacks = ResultCallbacks.Where( x => !x.Value.server ) - .ToDictionary( x => x.Key, x => x.Value ); + RemoveCallbacks(x => x.Value.server); } internal static void ShutdownClient() @@ -327,8 +336,7 @@ namespace Steamworks Callbacks[callback.Key].RemoveAll( x => !x.server ); } - ResultCallbacks = ResultCallbacks.Where( x => x.Value.server ) - .ToDictionary( x => x.Key, x => x.Value ); + RemoveCallbacks(x => !x.Value.server); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs index 615b0f963..84d17aa29 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamAppList.cs @@ -49,7 +49,8 @@ namespace Steamworks #endregion internal int GetAppName( AppId nAppID, out string pchName ) { - IntPtr mempchName = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchName = memory; var returnValue = _GetAppName( Self, nAppID, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -62,7 +63,8 @@ namespace Steamworks #endregion internal int GetAppInstallDir( AppId nAppID, out string pchDirectory ) { - IntPtr mempchDirectory = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchDirectory = memory; var returnValue = _GetAppInstallDir( Self, nAppID, mempchDirectory, (1024 * 32) ); pchDirectory = Helpers.MemoryToString( mempchDirectory ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs index 8c3962029..baa03e68f 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs @@ -159,7 +159,8 @@ namespace Steamworks #endregion internal bool BGetDLCDataByIndex( int iDLC, ref AppId pAppID, [MarshalAs( UnmanagedType.U1 )] ref bool pbAvailable, out string pchName ) { - IntPtr mempchName = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchName = memory; var returnValue = _BGetDLCDataByIndex( Self, iDLC, ref pAppID, ref pbAvailable, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -203,7 +204,8 @@ namespace Steamworks #endregion internal bool GetCurrentBetaName( out string pchName ) { - IntPtr mempchName = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchName = memory; var returnValue = _GetCurrentBetaName( Self, mempchName, (1024 * 32) ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -239,7 +241,8 @@ namespace Steamworks #endregion internal uint GetAppInstallDir( AppId appID, out string pchFolder ) { - IntPtr mempchFolder = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchFolder = memory; var returnValue = _GetAppInstallDir( Self, appID, mempchFolder, (1024 * 32) ); pchFolder = Helpers.MemoryToString( mempchFolder ); return returnValue; @@ -330,7 +333,8 @@ namespace Steamworks #endregion internal int GetLaunchCommandLine( out string pszCommandLine ) { - IntPtr mempszCommandLine = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempszCommandLine = memory; var returnValue = _GetLaunchCommandLine( Self, mempszCommandLine, (1024 * 32) ); pszCommandLine = Helpers.MemoryToString( mempszCommandLine ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs index 03d814f98..bad5eb31b 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamGameSearch.cs @@ -82,7 +82,8 @@ namespace Steamworks #endregion internal GameSearchErrorCode_t RetrieveConnectionDetails( SteamId steamIDHost, out string pchConnectionDetails ) { - IntPtr mempchConnectionDetails = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchConnectionDetails = memory; var returnValue = _RetrieveConnectionDetails( Self, steamIDHost, mempchConnectionDetails, (1024 * 32) ); pchConnectionDetails = Helpers.MemoryToString( mempchConnectionDetails ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs index 4cba05095..9adbeae02 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs @@ -60,7 +60,8 @@ namespace Steamworks #endregion internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { - IntPtr mempchValueBuffer = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchValueBuffer = memory; var returnValue = _GetResultItemProperty( Self, resultHandle, unItemIndex, pchPropertyName, mempchValueBuffer, ref punValueBufferSizeOut ); pchValueBuffer = Helpers.MemoryToString( mempchValueBuffer ); return returnValue; @@ -327,7 +328,8 @@ namespace Steamworks #endregion internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { - IntPtr mempchValueBuffer = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchValueBuffer = memory; var returnValue = _GetItemDefinitionProperty( Self, iDefinition, pchPropertyName, mempchValueBuffer, ref punValueBufferSizeOut ); pchValueBuffer = Helpers.MemoryToString( mempchValueBuffer ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs index f5f61f326..593676092 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamMatchmaking.cs @@ -266,8 +266,10 @@ namespace Steamworks #endregion internal bool GetLobbyDataByIndex( SteamId steamIDLobby, int iLobbyData, out string pchKey, out string pchValue ) { - IntPtr mempchKey = Helpers.TakeMemory(); - IntPtr mempchValue = Helpers.TakeMemory(); + using var memoryKey = Helpers.TakeMemory(); + using var memoryValue = Helpers.TakeMemory(); + IntPtr mempchKey = memoryKey; + IntPtr mempchValue = memoryValue; var returnValue = _GetLobbyDataByIndex( Self, steamIDLobby, iLobbyData, mempchKey, (1024 * 32), mempchValue, (1024 * 32) ); pchKey = Helpers.MemoryToString( mempchKey ); pchValue = Helpers.MemoryToString( mempchValue ); diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs index 1b208683a..61190cd7b 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingSockets.cs @@ -143,7 +143,8 @@ namespace Steamworks #endregion internal bool GetConnectionName( Connection hPeer, out string pszName ) { - IntPtr mempszName = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempszName = memory; var returnValue = _GetConnectionName( Self, hPeer, mempszName, (1024 * 32) ); pszName = Helpers.MemoryToString( mempszName ); return returnValue; @@ -223,7 +224,8 @@ namespace Steamworks #endregion internal int GetDetailedConnectionStatus( Connection hConn, out string pszBuf ) { - IntPtr mempszBuf = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempszBuf = memory; var returnValue = _GetDetailedConnectionStatus( Self, hConn, mempszBuf, (1024 * 32) ); pszBuf = Helpers.MemoryToString( mempszBuf ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs index c08abff8c..51739641e 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs @@ -92,7 +92,8 @@ namespace Steamworks #endregion internal void ConvertPingLocationToString( ref NetPingLocation location, out string pszBuf ) { - IntPtr mempszBuf = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempszBuf = memory; _ConvertPingLocationToString( Self, ref location, mempszBuf, (1024 * 32) ); pszBuf = Helpers.MemoryToString( mempszBuf ); } @@ -323,7 +324,8 @@ namespace Steamworks #endregion internal void SteamNetworkingIPAddr_ToString( ref NetAddress addr, out string buf, [MarshalAs( UnmanagedType.U1 )] bool bWithPort ) { - IntPtr membuf = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr membuf = memory; _SteamNetworkingIPAddr_ToString( Self, ref addr, membuf, (1024 * 32), bWithPort ); buf = Helpers.MemoryToString( membuf ); } @@ -347,7 +349,8 @@ namespace Steamworks #endregion internal void SteamNetworkingIdentity_ToString( ref NetIdentity identity, out string buf ) { - IntPtr membuf = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr membuf = memory; _SteamNetworkingIdentity_ToString( Self, ref identity, membuf, (1024 * 32) ); buf = Helpers.MemoryToString( membuf ); } diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs index 3740561fd..6adabea80 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamParties.cs @@ -50,7 +50,8 @@ namespace Steamworks #endregion internal bool GetBeaconDetails( PartyBeaconID_t ulBeaconID, ref SteamId pSteamIDBeaconOwner, ref SteamPartyBeaconLocation_t pLocation, out string pchMetadata ) { - IntPtr mempchMetadata = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchMetadata = memory; var returnValue = _GetBeaconDetails( Self, ulBeaconID, ref pSteamIDBeaconOwner, ref pLocation, mempchMetadata, (1024 * 32) ); pchMetadata = Helpers.MemoryToString( mempchMetadata ); return returnValue; @@ -153,7 +154,8 @@ namespace Steamworks #endregion internal bool GetBeaconLocationData( SteamPartyBeaconLocation_t BeaconLocation, SteamPartyBeaconLocationData eData, out string pchDataStringOut ) { - IntPtr mempchDataStringOut = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchDataStringOut = memory; var returnValue = _GetBeaconLocationData( Self, BeaconLocation, eData, mempchDataStringOut, (1024 * 32) ); pchDataStringOut = Helpers.MemoryToString( mempchDataStringOut ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs index cf1582003..dbe560c05 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs @@ -98,7 +98,8 @@ namespace Steamworks #endregion internal bool GetQueryUGCPreviewURL( UGCQueryHandle_t handle, uint index, out string pchURL ) { - IntPtr mempchURL = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchURL = memory; var returnValue = _GetQueryUGCPreviewURL( Self, handle, index, mempchURL, (1024 * 32) ); pchURL = Helpers.MemoryToString( mempchURL ); return returnValue; @@ -112,7 +113,8 @@ namespace Steamworks #endregion internal bool GetQueryUGCMetadata( UGCQueryHandle_t handle, uint index, out string pchMetadata ) { - IntPtr mempchMetadata = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchMetadata = memory; var returnValue = _GetQueryUGCMetadata( Self, handle, index, mempchMetadata, (1024 * 32) ); pchMetadata = Helpers.MemoryToString( mempchMetadata ); return returnValue; @@ -161,8 +163,10 @@ namespace Steamworks #endregion internal bool GetQueryUGCAdditionalPreview( UGCQueryHandle_t handle, uint index, uint previewIndex, out string pchURLOrVideoID, out string pchOriginalFileName, ref ItemPreviewType pPreviewType ) { - IntPtr mempchURLOrVideoID = Helpers.TakeMemory(); - IntPtr mempchOriginalFileName = Helpers.TakeMemory(); + using var memoryUrlOrId = Helpers.TakeMemory(); + using var memoryFileName = Helpers.TakeMemory(); + IntPtr mempchURLOrVideoID = memoryUrlOrId; + IntPtr mempchOriginalFileName = memoryFileName; var returnValue = _GetQueryUGCAdditionalPreview( Self, handle, index, previewIndex, mempchURLOrVideoID, (1024 * 32), mempchOriginalFileName, (1024 * 32), ref pPreviewType ); pchURLOrVideoID = Helpers.MemoryToString( mempchURLOrVideoID ); pchOriginalFileName = Helpers.MemoryToString( mempchOriginalFileName ); @@ -188,8 +192,10 @@ namespace Steamworks #endregion internal bool GetQueryUGCKeyValueTag( UGCQueryHandle_t handle, uint index, uint keyValueTagIndex, out string pchKey, out string pchValue ) { - IntPtr mempchKey = Helpers.TakeMemory(); - IntPtr mempchValue = Helpers.TakeMemory(); + using var memoryKey = Helpers.TakeMemory(); + using var memoryValue = Helpers.TakeMemory(); + IntPtr mempchKey = memoryKey; + IntPtr mempchValue = memoryValue; var returnValue = _GetQueryUGCKeyValueTag( Self, handle, index, keyValueTagIndex, mempchKey, (1024 * 32), mempchValue, (1024 * 32) ); pchKey = Helpers.MemoryToString( mempchKey ); pchValue = Helpers.MemoryToString( mempchValue ); @@ -204,7 +210,8 @@ namespace Steamworks #endregion internal bool GetQueryUGCKeyValueTag( UGCQueryHandle_t handle, uint index, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchKey, out string pchValue ) { - IntPtr mempchValue = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchValue = memory; var returnValue = _GetQueryUGCKeyValueTag( Self, handle, index, pchKey, mempchValue, (1024 * 32) ); pchValue = Helpers.MemoryToString( mempchValue ); return returnValue; @@ -793,7 +800,8 @@ namespace Steamworks #endregion internal bool GetItemInstallInfo( PublishedFileId nPublishedFileID, ref ulong punSizeOnDisk, out string pchFolder, ref uint punTimeStamp ) { - IntPtr mempchFolder = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchFolder = memory; var returnValue = _GetItemInstallInfo( Self, nPublishedFileID, ref punSizeOnDisk, mempchFolder, (1024 * 32), ref punTimeStamp ); pchFolder = Helpers.MemoryToString( mempchFolder ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs index 470239152..77624f5f8 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUser.cs @@ -93,7 +93,8 @@ namespace Steamworks #endregion internal bool GetUserDataFolder( out string pchBuffer ) { - IntPtr mempchBuffer = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchBuffer = memory; var returnValue = _GetUserDataFolder( Self, mempchBuffer, (1024 * 32) ); pchBuffer = Helpers.MemoryToString( mempchBuffer ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs index 707ad7629..69d46180b 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUserStats.cs @@ -433,7 +433,8 @@ namespace Steamworks #endregion internal int GetMostAchievedAchievementInfo( out string pchName, ref float pflPercent, [MarshalAs( UnmanagedType.U1 )] ref bool pbAchieved ) { - IntPtr mempchName = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchName = memory; var returnValue = _GetMostAchievedAchievementInfo( Self, mempchName, (1024 * 32), ref pflPercent, ref pbAchieved ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; @@ -446,7 +447,8 @@ namespace Steamworks #endregion internal int GetNextMostAchievedAchievementInfo( int iIteratorPrevious, out string pchName, ref float pflPercent, [MarshalAs( UnmanagedType.U1 )] ref bool pbAchieved ) { - IntPtr mempchName = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchName = memory; var returnValue = _GetNextMostAchievedAchievementInfo( Self, iIteratorPrevious, mempchName, (1024 * 32), ref pflPercent, ref pbAchieved ); pchName = Helpers.MemoryToString( mempchName ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs index f091960d3..aae959ae4 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamUtils.cs @@ -268,7 +268,8 @@ namespace Steamworks #endregion internal bool GetEnteredGamepadTextInput( out string pchText ) { - IntPtr mempchText = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchText = memory; var returnValue = _GetEnteredGamepadTextInput( Self, mempchText, (1024 * 32) ); pchText = Helpers.MemoryToString( mempchText ); return returnValue; @@ -382,7 +383,8 @@ namespace Steamworks #endregion internal int FilterText( out string pchOutFilteredText, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchInputMessage, [MarshalAs( UnmanagedType.U1 )] bool bLegalOnly ) { - IntPtr mempchOutFilteredText = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchOutFilteredText = memory; var returnValue = _FilterText( Self, mempchOutFilteredText, (1024 * 32), pchInputMessage, bLegalOnly ); pchOutFilteredText = Helpers.MemoryToString( mempchOutFilteredText ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs index 28a9c13d6..c6c55105e 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamVideo.cs @@ -60,7 +60,8 @@ namespace Steamworks #endregion internal bool GetOPFStringForApp( AppId unVideoAppID, out string pchBuffer, ref int pnBufferSize ) { - IntPtr mempchBuffer = Helpers.TakeMemory(); + using var memory = Helpers.TakeMemory(); + IntPtr mempchBuffer = memory; var returnValue = _GetOPFStringForApp( Self, unVideoAppID, mempchBuffer, ref pnBufferSize ); pchBuffer = Helpers.MemoryToString( mempchBuffer ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs b/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs index cc47166a6..09db1e883 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetAddress.cs @@ -147,7 +147,7 @@ namespace Steamworks.Data public override string ToString() { - var ptr = Helpers.TakeMemory(); + using var ptr = Helpers.TakeMemory(); var self = this; InternalToString( ref self, ptr, Helpers.MemoryBufferSize, true ); return Helpers.MemoryToString( ptr ); diff --git a/Libraries/Facepunch.Steamworks/SteamFriends.cs b/Libraries/Facepunch.Steamworks/SteamFriends.cs index 9d3632607..36a57e6cc 100644 --- a/Libraries/Facepunch.Steamworks/SteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/SteamFriends.cs @@ -83,7 +83,7 @@ namespace Steamworks var friend = new Friend( data.SteamIDUser ); - var buffer = Helpers.TakeMemory(); + using var buffer = Helpers.TakeMemory(); var type = ChatEntryType.ChatMsg; var len = Internal.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ); diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs index 10374743c..ff3857a6f 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs @@ -72,7 +72,7 @@ namespace Steamworks { SteamId steamid = default; ChatEntryType chatEntryType = default; - var buffer = Helpers.TakeMemory(); + using var buffer = Helpers.TakeMemory(); var readData = Internal.GetLobbyChatEntry( callback.SteamIDLobby, (int)callback.ChatID, ref steamid, buffer, Helpers.MemoryBufferSize, ref chatEntryType ); diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 03ada053c..db06eeae8 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -29,14 +29,7 @@ namespace Steamworks { if (x.AppID == SteamClient.AppId) { - OnDownloadItemResult?.Invoke(x.Result); - - Ugc.Item item = new Ugc.Item(x.PublishedFileId); - if (item.IsInstalled && (onItemInstalled?.ContainsKey(x.PublishedFileId) ?? false)) - { - onItemInstalled[x.PublishedFileId]?.Invoke(); - onItemInstalled.Remove(x.PublishedFileId); - } + OnDownloadItemResult?.Invoke(x.Result, x.PublishedFileId); } }, server ); Dispatch.Install(x => @@ -44,11 +37,6 @@ namespace Steamworks if (x.AppID == SteamClient.AppId) { GlobalOnItemInstalled?.Invoke(x.PublishedFileId); - if (onItemInstalled?.ContainsKey(x.PublishedFileId) ?? false) - { - onItemInstalled[x.PublishedFileId]?.Invoke(); - onItemInstalled.Remove(x.PublishedFileId); - } } }, server); } @@ -56,7 +44,7 @@ namespace Steamworks /// /// Posted after Download call /// - public static event Action OnDownloadItemResult; + public static event Action OnDownloadItemResult; public static async Task DeleteFileAsync( PublishedFileId fileId ) { @@ -70,20 +58,8 @@ namespace Steamworks /// The ID of the file you want to download /// If true this should go straight to the top of the download list /// true if nothing went wrong and the download is started - public static bool Download( PublishedFileId fileId, Action onInstalled = null, bool highPriority = false ) + public static bool Download( PublishedFileId fileId, bool highPriority = false ) { - if (onInstalled != null) - { - onItemInstalled ??= new Dictionary(); - if (!onItemInstalled.ContainsKey(fileId)) - { - onItemInstalled.Add(fileId, onInstalled); - } - else - { - onItemInstalled[fileId] += onInstalled; - } - } return Internal.DownloadItem( fileId, highPriority ); } @@ -104,7 +80,7 @@ namespace Steamworks progress?.Invoke( 0.0f ); - if ( Download( fileId, null, true ) == false ) + if ( Download( fileId, highPriority: true ) == false ) return item.IsInstalled; // Steam docs about Download: @@ -114,13 +90,13 @@ namespace Steamworks // Wait for DownloadItemResult_t { - Action onDownloadStarted = null; + Action onDownloadStarted = null; try { var downloadStarted = false; - onDownloadStarted = r => downloadStarted = true; + onDownloadStarted = (r, id) => downloadStarted = true; OnDownloadItemResult += onDownloadStarted; while ( downloadStarted == false ) @@ -199,9 +175,7 @@ namespace Steamworks return result.Value.Result == Result.OK; } - private static Dictionary onItemInstalled; - - public static event Action GlobalOnItemInstalled; + public static Action GlobalOnItemInstalled; public static uint NumSubscribedItems { get { return Internal.GetNumSubscribedItems(); } } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs index 1c06a8f90..9acaf229c 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs @@ -69,19 +69,13 @@ namespace Steamworks.Ugc public Editor WithContent( System.IO.DirectoryInfo t ) { this.ContentFolder = t; return this; } public Editor WithContent( string folderName ) { return WithContent( new System.IO.DirectoryInfo( folderName ) ); } - RemoteStoragePublishedFileVisibility? Visibility; + public Visibility? Visibility; - public Editor WithPublicVisibility() { Visibility = RemoteStoragePublishedFileVisibility.Public; return this; } - public Editor WithFriendsOnlyVisibility() { Visibility = RemoteStoragePublishedFileVisibility.FriendsOnly; return this; } - public Editor WithPrivateVisibility() { Visibility = RemoteStoragePublishedFileVisibility.Private; return this; } + public Editor WithVisibility(Visibility visibility) { Visibility = visibility; return this; } public List Tags { get; private set; } - Dictionary> KeyValueTags; - HashSet KeyValueTagsToRemove; - - public bool IsPublic => Visibility == RemoteStoragePublishedFileVisibility.Public; - public bool IsFriendsOnly => Visibility == RemoteStoragePublishedFileVisibility.FriendsOnly; - public bool IsPrivate => Visibility == RemoteStoragePublishedFileVisibility.Private; + Dictionary> keyValueTags; + HashSet keyValueTagsToRemove; public Editor WithTag( string tag ) { @@ -117,13 +111,13 @@ namespace Steamworks.Ugc /// public Editor AddKeyValueTag(string key, string value) { - if (KeyValueTags == null) - KeyValueTags = new Dictionary>(); + if (keyValueTags == null) + keyValueTags = new Dictionary>(); - if ( KeyValueTags.TryGetValue( key, out var list ) ) + if ( keyValueTags.TryGetValue( key, out var list ) ) list.Add( value ); else - KeyValueTags[key] = new List() { value }; + keyValueTags[key] = new List() { value }; return this; } @@ -135,10 +129,10 @@ namespace Steamworks.Ugc /// public Editor RemoveKeyValueTags(string key) { - if (KeyValueTagsToRemove == null) - KeyValueTagsToRemove = new HashSet(); + if (keyValueTagsToRemove == null) + keyValueTagsToRemove = new HashSet(); - KeyValueTagsToRemove.Add(key); + keyValueTagsToRemove.Add(key); return this; } @@ -207,7 +201,7 @@ namespace Steamworks.Ugc if ( Language != null ) SteamUGC.Internal.SetItemUpdateLanguage( handle, Language ); if ( ContentFolder != null ) SteamUGC.Internal.SetItemContent( handle, ContentFolder.FullName ); if ( PreviewFile != null ) SteamUGC.Internal.SetItemPreview( handle, PreviewFile ); - if ( Visibility.HasValue ) SteamUGC.Internal.SetItemVisibility( handle, Visibility.Value ); + if ( Visibility.HasValue ) SteamUGC.Internal.SetItemVisibility( handle, (RemoteStoragePublishedFileVisibility)Visibility.Value ); if ( Tags != null && Tags.Count > 0 ) { using ( var a = SteamParamStringArray.From( Tags.ToArray() ) ) @@ -217,15 +211,15 @@ namespace Steamworks.Ugc } } - if ( KeyValueTagsToRemove != null) + if ( keyValueTagsToRemove != null) { - foreach ( var key in KeyValueTagsToRemove ) + foreach ( var key in keyValueTagsToRemove ) SteamUGC.Internal.RemoveItemKeyValueTags( handle, key ); } - if ( KeyValueTags != null ) + if ( keyValueTags != null ) { - foreach ( var keyWithValues in KeyValueTags ) + foreach ( var keyWithValues in keyValueTags ) { var key = keyWithValues.Key; foreach ( var value in keyWithValues.Value ) diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 894f641f2..742cfbf83 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -9,6 +9,14 @@ using QueryType = Steamworks.Ugc.Query; namespace Steamworks.Ugc { + public enum Visibility : int + { + Public = 0, + FriendsOnly = 1, + Private = 2, + Unlisted = 3, + } + public struct Item { internal SteamUGCDetails_t details; @@ -74,20 +82,20 @@ namespace Steamworks.Ugc /// public DateTime Updated => Epoch.ToDateTime( details.TimeUpdated ); - /// - /// True if this is publically visible - /// - public bool IsPublic => details.Visibility == RemoteStoragePublishedFileVisibility.Public; + public DateTime LatestUpdateTime + { + get + { + var created = Created; + var updated = Updated; + return created > updated ? created : updated; + } + } /// - /// True if this item is only visible by friends of the creator + /// The item's visibility, i.e. public, friends-only, unlisted or private /// - public bool IsFriendsOnly => details.Visibility == RemoteStoragePublishedFileVisibility.FriendsOnly; - - /// - /// True if this is only visible to the creator - /// - public bool IsPrivate => details.Visibility == RemoteStoragePublishedFileVisibility.Private; + public Visibility Visibility => (Visibility)details.Visibility; /// /// True if this item has been banned @@ -122,22 +130,11 @@ namespace Steamworks.Ugc ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out var strVal, ref ts ) ) - return null; - - return strVal; + if (SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } + return null; } } - /// - /// Start downloading this item. - /// If this returns false the item isn't getting downloaded. - /// - public bool Download( Action onInstalled = null, bool highPriority = false ) - { - return SteamUGC.Download( Id, onInstalled, highPriority ); - } - /// /// If we're downloading, how big the total download is /// @@ -146,7 +143,7 @@ namespace Steamworks.Ugc get { if ( !NeedsUpdate ) - return SizeBytes; + return InstalledSize; ulong downloaded = 0; ulong total = 0; @@ -165,7 +162,7 @@ namespace Steamworks.Ugc get { if ( !NeedsUpdate ) - return SizeBytes; + return InstalledSize; ulong downloaded = 0; ulong total = 0; @@ -179,7 +176,7 @@ namespace Steamworks.Ugc /// /// If we're installed, how big is the install /// - public long SizeBytes + public long InstalledSize { get { @@ -195,6 +192,13 @@ namespace Steamworks.Ugc } } + /// + /// File size as returned by Steamworks, + /// no download/install required + /// + public long SizeOfFileInBytes + => details.FileSize; + /// /// If we're downloading our current progress as a delta betwen 0-1 /// @@ -204,17 +208,19 @@ namespace Steamworks.Ugc { //changed from NeedsUpdate as it's false when validating and redownloading ugc //possibly similar properties should also be changed - if ( !IsDownloading ) return 1; - ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) && total > 0 ) + if (SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) + { return (float)((double)downloaded / (double)total); + } - if ( NeedsUpdate || !IsInstalled || IsDownloading ) + if (NeedsUpdate || !IsInstalled || IsDownloading) + { return 0; + } - return 1; + return IsDownloadPending || IsDownloading ? 0 : 1; } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs index f59d22ee3..b8bc42740 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -15,7 +15,6 @@ namespace Steamworks.Ugc AppId consumerApp; AppId creatorApp; string searchText; - bool returnLongDescription; public Query( UgcType type ) : this() { @@ -106,18 +105,6 @@ namespace Steamworks.Ugc } #endregion - public Query WithLongDescription() - { - returnLongDescription = true; - return this; - } - - public Query WithSummaryDescription() - { - returnLongDescription = false; - return this; - } - public async Task GetPageAsync( int page ) { if ( page <= 0 ) throw new System.Exception( "page should be > 0" ); @@ -255,8 +242,6 @@ namespace Steamworks.Ugc { SteamUGC.Internal.SetSearchText( handle, searchText ); } - - SteamUGC.Internal.SetReturnLongDescription( handle, returnLongDescription ); } #endregion diff --git a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs index 03eb9153c..968e9d0ea 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs @@ -2,6 +2,8 @@ using System; using System.Runtime.InteropServices; using System.Text; using System.Collections.Generic; +using System.Threading; +using System.Collections.Concurrent; namespace Steamworks { @@ -9,33 +11,40 @@ namespace Steamworks { public const int MemoryBufferSize = 1024 * 32; - private static IntPtr[] MemoryPool = new IntPtr[] - { - Marshal.AllocHGlobal( MemoryBufferSize ), - Marshal.AllocHGlobal( MemoryBufferSize ), - Marshal.AllocHGlobal( MemoryBufferSize ), - Marshal.AllocHGlobal( MemoryBufferSize ) - }; + internal struct Memory : IDisposable + { + private const int MaxBagSize = 4; + private static readonly ConcurrentBag BufferBag = new ConcurrentBag(); - private static int MemoryPoolIndex; + public IntPtr Ptr { get; private set; } - public static unsafe IntPtr TakeMemory() - { - lock ( MemoryPool ) + public static implicit operator IntPtr(in Memory m) => m.Ptr; + + internal unsafe Memory(int sz) { - MemoryPoolIndex++; - - if ( MemoryPoolIndex >= MemoryPool.Length ) - MemoryPoolIndex = 0; - - var take = MemoryPool[MemoryPoolIndex]; - - ((byte*)take)[0] = 0; - - return take; + Ptr = BufferBag.TryTake(out IntPtr ptr) ? ptr : Marshal.AllocHGlobal(sz); + ((byte*)Ptr)[0] = 0; } - } + public void Dispose() + { + if (Ptr == IntPtr.Zero) { return; } + if (BufferBag.Count < MaxBagSize) + { + BufferBag.Add(Ptr); + } + else + { + Marshal.FreeHGlobal(Ptr); + } + Ptr = IntPtr.Zero; + } + } + + public static Memory TakeMemory() + { + return new Memory(MemoryBufferSize); + } private static byte[][] BufferPool = new byte[4][]; private static int BufferPoolIndex; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs index 6c08c2979..f49bdecfd 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs @@ -2,34 +2,34 @@ // /* // Microsoft Public License (Ms-PL) // MonoGame - Copyright © 2009 The MonoGame Team -// +// // All rights reserved. -// +// // This license governs use of the accompanying software. If you use the software, you accept this license. If you do not // accept the license, do not use the software. -// +// // 1. Definitions -// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under // U.S. copyright law. -// +// // A "contribution" is the original software, or any additions or changes to the software. // A "contributor" is any person that distributes its contribution under this license. // "Licensed patents" are a contributor's patent claims that read directly on its contribution. -// +// // 2. Grant of Rights -// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, // each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. -// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, // each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. -// +// // 3. Conditions and Limitations // (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. -// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, // your patent license from such contributor to the software ends automatically. -// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution // notices that are present in the software. -// (D) If you distribute any portion of the software in source code form, you may do so only under this license by including -// a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +// (D) If you distribute any portion of the software in source code form, you may do so only under this license by including +// a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object // code form, you may only do so under a license that complies with this license. // (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees // or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent @@ -56,7 +56,7 @@ namespace Microsoft.Xna.Framework.Graphics internal GraphicsResource() { - + } ~GraphicsResource() @@ -66,7 +66,7 @@ namespace Microsoft.Xna.Framework.Graphics } /// - /// Called before the device is reset. Allows graphics resources to + /// Called before the device is reset. Allows graphics resources to /// invalidate their state so they can be recreated after the device reset. /// Warning: This may be called after a call to Dispose() up until /// the resource is garbage collected. @@ -117,7 +117,7 @@ namespace Microsoft.Xna.Framework.Graphics } public event EventHandler Disposing; - + public GraphicsDevice GraphicsDevice { get @@ -146,7 +146,7 @@ namespace Microsoft.Xna.Framework.Graphics graphicsDevice.AddResourceReference(_selfReference); } } - + public bool IsDisposed { get @@ -154,9 +154,9 @@ namespace Microsoft.Xna.Framework.Graphics return disposed; } } - + public string Name { get; set; } - + public Object Tag { get; set; } public override string ToString() diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index d6560df72..441214b6e 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -237,9 +237,17 @@ namespace Microsoft.Xna.Framework.Graphics void CheckValid(Texture2D texture) { if (texture == null) + { throw new ArgumentNullException("texture"); + } + if (texture.IsDisposed) + { + throw new InvalidOperationException($"Texture is disposed"); + } if (!_beginCalled) + { throw new InvalidOperationException("Draw was called, but Begin has not yet been called. Begin must be called successfully before you can call Draw."); + } } void CheckValid(SpriteFont spriteFont, string text) @@ -315,7 +323,7 @@ namespace Microsoft.Xna.Framework.Graphics } } - public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth) + public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth, int? count = null) { CheckValid(texture); @@ -338,7 +346,7 @@ namespace Microsoft.Xna.Framework.Graphics break; } - int iters = vertices.Length / 4; + int iters = count ?? (vertices.Length / 4); for (int i=0;i 4, + Keys.B => 5, + Keys.C => 6, + Keys.D => 7, + Keys.E => 8, + Keys.F => 9, + Keys.G => 10, + Keys.H => 11, + Keys.I => 12, + Keys.J => 13, + Keys.K => 14, + Keys.L => 15, + Keys.M => 16, + Keys.N => 17, + Keys.O => 18, + Keys.P => 19, + Keys.Q => 20, + Keys.R => 21, + Keys.S => 22, + Keys.T => 23, + Keys.U => 24, + Keys.V => 25, + Keys.W => 26, + Keys.X => 27, + Keys.Y => 28, + Keys.Z => 29, + _ => -1 + }; + if (scancode < 0) { return qwertyKey; } + return KeyboardUtil.ToXna(Sdl.Keyboard.GetKeyFromScancode(scancode)); + } } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs index 0da7ec4c5..5ee24f7f0 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs @@ -837,6 +837,10 @@ internal static class Sdl [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate Keymod d_sdl_getmodstate(); public static d_sdl_getmodstate GetModState = FuncLoader.LoadFunction(NativeLibrary, "SDL_GetModState"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int d_sdl_getkeyfromscancode(int scancode); + public static d_sdl_getkeyfromscancode GetKeyFromScancode = FuncLoader.LoadFunction(NativeLibrary, "SDL_GetKeyFromScancode"); } public static class Joystick diff --git a/Libraries/XNATypes/Point.cs b/Libraries/XNATypes/Point.cs index 671017b63..314e769ce 100644 --- a/Libraries/XNATypes/Point.cs +++ b/Libraries/XNATypes/Point.cs @@ -153,6 +153,7 @@ namespace Microsoft.Xna.Framework return !a.Equals(b); } + public static implicit operator Point((int X, int Y) tuple) => new Point(tuple.X, tuple.Y); #endregion #region Public methods diff --git a/Libraries/XNATypes/Vector2.cs b/Libraries/XNATypes/Vector2.cs index cc7c02161..74b600963 100644 --- a/Libraries/XNATypes/Vector2.cs +++ b/Libraries/XNATypes/Vector2.cs @@ -247,6 +247,7 @@ namespace Microsoft.Xna.Framework return value1.X != value2.X || value1.Y != value2.Y; } + public static implicit operator Vector2((float X, float Y) tuple) => new Vector2(tuple.X, tuple.Y); #endregion #region Public Methods diff --git a/Libraries/XNATypes/Vector3.cs b/Libraries/XNATypes/Vector3.cs index 5137ef7d2..5d35750ff 100644 --- a/Libraries/XNATypes/Vector3.cs +++ b/Libraries/XNATypes/Vector3.cs @@ -1368,6 +1368,7 @@ namespace Microsoft.Xna.Framework return value1; } + public static implicit operator Vector3((float X, float Y, float Z) tuple) => new Vector3(tuple.X, tuple.Y, tuple.Z); #endregion } } diff --git a/Libraries/XNATypes/Vector4.cs b/Libraries/XNATypes/Vector4.cs index 9fcc2f72a..f05c39b02 100644 --- a/Libraries/XNATypes/Vector4.cs +++ b/Libraries/XNATypes/Vector4.cs @@ -1292,6 +1292,7 @@ namespace Microsoft.Xna.Framework return value1; } + public static implicit operator Vector4((float X, float Y, float Z, float W) tuple) => new Vector4(tuple.X, tuple.Y, tuple.Z, tuple.W); #endregion } }