diff --git a/Barotrauma/BarotraumaClient/ClientCode.projitems b/Barotrauma/BarotraumaClient/ClientCode.projitems index 7c88d6ccd..37b214100 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.projitems +++ b/Barotrauma/BarotraumaClient/ClientCode.projitems @@ -36,6 +36,7 @@ + @@ -193,7 +194,8 @@ - + + @@ -217,6 +219,7 @@ + @@ -227,9 +230,9 @@ + - - + diff --git a/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs index 485c6381a..c89a88b51 100644 --- a/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.9.3.2")] -[assembly: AssemblyFileVersion("0.9.3.2")] +[assembly: AssemblyVersion("0.9.4.0")] +[assembly: AssemblyFileVersion("0.9.4.0")] diff --git a/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs index a44a18f66..4d6c90904 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs @@ -45,9 +45,6 @@ namespace Barotrauma case AIState.Eat: stateColor = Color.Brown; break; - case AIState.GoTo: - stateColor = Color.Magenta; - break; } GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 80.0f, State.ToString(), stateColor, Color.Black); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs index f6571cde5..613bf9d15 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs @@ -10,7 +10,7 @@ namespace Barotrauma { /*if (GameMain.GameSession != null && GameMain.GameSession.CrewManager != null) { - CurrentOrder = Order.PrefabList.Find(o => o.AITag == "dismissed"); + CurrentOrder = Order.GetPrefab("dismissed"); objectiveManager.SetOrder(CurrentOrder, "", null); GameMain.GameSession.CrewManager.SetCharacterOrder(Character, CurrentOrder, null, null); }*/ diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs index aae0a57f9..c42fb71fb 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs @@ -369,12 +369,12 @@ namespace Barotrauma LimbJoints.ForEach(j => j.UpdateDeformations(deltaTime)); foreach (var deformation in SpriteDeformations) { - if (character.IsDead && deformation.DeformationParams.StopWhenHostIsDead) { continue; } - if (deformation.DeformationParams.UseMovementSine) + if (character.IsDead && deformation.Params.StopWhenHostIsDead) { continue; } + if (deformation.Params.UseMovementSine) { if (this is AnimController animator) { - deformation.Phase = MathUtils.WrapAngleTwoPi(animator.WalkPos * deformation.DeformationParams.Frequency + MathHelper.Pi * deformation.DeformationParams.SineOffset); + deformation.Phase = MathUtils.WrapAngleTwoPi(animator.WalkPos * deformation.Params.Frequency + MathHelper.Pi * deformation.Params.SineOffset); } } else diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Attack.cs b/Barotrauma/BarotraumaClient/Source/Characters/Attack.cs index 3b3626b65..84d76d1e8 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Attack.cs @@ -7,10 +7,8 @@ namespace Barotrauma { partial class Attack { - public string StructureSoundType - { - get; private set; - } + [Serialize("StructureBlunt", true), Editable()] + public string StructureSoundType { get; private set; } private RoundSound sound; @@ -23,8 +21,6 @@ namespace Barotrauma DebugConsole.ThrowError("Error in attack ("+element+") - sounds should be defined as child elements, not as attributes."); return; } - - StructureSoundType = element.GetAttributeString("structuresoundtype", "StructureBlunt"); foreach (XElement subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs index 5c36aae9e..cf53dd16f 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs @@ -86,11 +86,7 @@ namespace Barotrauma set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } } - public string BloodDecalName - { - get; - private set; - } + public string BloodDecalName => Params.BloodDecal; private List bloodEmitters = new List(); public IEnumerable BloodEmitters @@ -137,22 +133,18 @@ namespace Barotrauma get { return activeObjectiveEntities; } } - partial void InitProjSpecific(XDocument doc) + partial void InitProjSpecific(XElement mainElement) { - soundInterval = doc.Root.GetAttributeFloat("soundinterval", 10.0f); + soundInterval = mainElement.GetAttributeFloat("soundinterval", 10.0f); soundTimer = Rand.Range(0.0f, soundInterval); - BloodDecalName = doc.Root.GetAttributeString("blooddecal", ""); - sounds = new List(); - foreach (XElement subElement in doc.Root.Elements()) + Params.Sounds.ForEach(s => sounds.Add(new CharacterSound(s))); + + foreach (XElement subElement in mainElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { - case "sound": - var characterSound = new CharacterSound(subElement); - if (characterSound.Sound != null) { sounds.Add(characterSound); } - break; case "damageemitter": damageEmitters.Add(new ParticleEmitter(subElement)); break; @@ -216,7 +208,7 @@ namespace Barotrauma float targetOffsetAmount = 0.0f; if (moveCam) { - if (needsAir && + if (NeedsAir && pressureProtection < 80.0f && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { @@ -351,7 +343,7 @@ namespace Barotrauma partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult) { - if (attackResult.Damage <= 0) { return; } + if (attackResult.Damage <= 1.0f) { return; } if (soundTimer < soundInterval * 0.5f) { PlaySound(CharacterSound.SoundType.Damage); @@ -542,7 +534,7 @@ namespace Barotrauma { switch (AIController.State) { - case AIController.AIState.Attack: + case AIState.Attack: PlaySound(CharacterSound.SoundType.Attack); break; default: @@ -577,6 +569,7 @@ namespace Barotrauma } } + CharacterHealth.UpdateClientSpecific(deltaTime); if (controlled == this) { CharacterHealth.UpdateHUD(deltaTime); @@ -612,7 +605,7 @@ namespace Barotrauma CharacterHUD.Draw(spriteBatch, this, cam); if (drawHealth) CharacterHealth.DrawHUD(spriteBatch); } - + public virtual void DrawFront(SpriteBatch spriteBatch, Camera cam) { if (!Enabled) return; @@ -713,12 +706,14 @@ namespace Barotrauma if (IsDead) return; - if (Vitality < MaxVitality * 0.98f && hudInfoVisible) + if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { + hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); + Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), - Vitality / MaxVitality, - Color.Lerp(Color.Red, Color.Green, Vitality / MaxVitality) * 0.8f * hudInfoAlpha, + CharacterHealth.DisplayedVitality / MaxVitality, + Color.Lerp(Color.Red, Color.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs index 1b51261ea..0e7a8ad34 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs @@ -220,7 +220,7 @@ namespace Barotrauma } - public static CharacterInfo ClientRead(string configPath, IReadMessage inc) + public static CharacterInfo ClientRead(string speciesName, IReadMessage inc) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); @@ -238,7 +238,7 @@ namespace Barotrauma Dictionary skillLevels = new Dictionary(); if (!string.IsNullOrEmpty(jobIdentifier)) { - jobPrefab = JobPrefab.List.Find(jp => jp.Identifier == jobIdentifier); + jobPrefab = JobPrefab.Get(jobIdentifier); byte skillCount = inc.ReadByte(); for (int i = 0; i < skillCount; i++) { @@ -249,7 +249,7 @@ namespace Barotrauma } // TODO: animations - CharacterInfo ch = new CharacterInfo(configPath, newName, jobPrefab, ragdollFile) + CharacterInfo ch = new CharacterInfo(speciesName, newName, jobPrefab, ragdollFile) { ID = infoID, }; diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs index 522ed8426..f0e3a3aff 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs @@ -20,13 +20,13 @@ namespace Barotrauma return; } - //freeze AI characters if more than 1 seconds have passed since last update from the server - if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - 1.0f) + //freeze AI characters if more than x seconds have passed since last update from the server + if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - NetConfig.FreezeCharacterIfPositionDataMissingDelay) { AnimController.Frozen = true; memState.Clear(); - //hide after 2 seconds - if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - 2.0f) + //hide after y seconds + if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - NetConfig.DisableCharacterIfPositionDataMissingDelay) { Enabled = false; return; @@ -74,7 +74,7 @@ namespace Barotrauma states = newInput, intAim = intAngle }; - if (focusedItem != null && !CharacterInventory.DraggingItemToWorld && + if (focusedItem != null && !CharacterInventory.DraggingItemToWorld && (!newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health))) { newMem.interact = focusedItem.ID; @@ -133,9 +133,9 @@ namespace Barotrauma { msg.WriteRangedInteger((int)memInput[i].states, 0, (int)InputNetFlags.MaxVal); msg.Write(memInput[i].intAim); - if (memInput[i].states.HasFlag(InputNetFlags.Select) || + if (memInput[i].states.HasFlag(InputNetFlags.Select) || memInput[i].states.HasFlag(InputNetFlags.Deselect) || - memInput[i].states.HasFlag(InputNetFlags.Use) || + memInput[i].states.HasFlag(InputNetFlags.Use) || memInput[i].states.HasFlag(InputNetFlags.Health) || memInput[i].states.HasFlag(InputNetFlags.Grab)) { @@ -187,11 +187,11 @@ namespace Barotrauma bool attackInput = msg.ReadBoolean(); keys[(int)InputType.Attack].Held = attackInput; keys[(int)InputType.Attack].SetState(false, attackInput); - + double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI; cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f; TransformCursorPos(); - + bool ragdollInput = msg.ReadBoolean(); keys[(int)InputType.Ragdoll].Held = ragdollInput; keys[(int)InputType.Ragdoll].SetState(false, ragdollInput); @@ -225,7 +225,7 @@ namespace Barotrauma msg.ReadSingle()); float MaxVel = NetConfig.MaxPhysicsBodyVelocity; Vector2 linearVelocity = new Vector2( - msg.ReadRangedSingle(-MaxVel, MaxVel, 12), + msg.ReadRangedSingle(-MaxVel, MaxVel, 12), msg.ReadRangedSingle(-MaxVel, MaxVel, 12)); linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12); @@ -252,9 +252,9 @@ namespace Barotrauma if (GameMain.Client.Character == this && AllowInput) { var posInfo = new CharacterStateInfo( - pos, rotation, - networkUpdateID, - facingRight ? Direction.Right : Direction.Left, + pos, rotation, + networkUpdateID, + facingRight ? Direction.Right : Direction.Left, selectedCharacter, selectedItem, animation); while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID)) @@ -264,11 +264,11 @@ namespace Barotrauma else { var posInfo = new CharacterStateInfo( - pos, rotation, - linearVelocity, angularVelocity, - sendingTime, facingRight ? Direction.Right : Direction.Left, + pos, rotation, + linearVelocity, angularVelocity, + sendingTime, facingRight ? Direction.Right : Direction.Left, selectedCharacter, selectedItem, animation); - + while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp) index++; memState.Insert(index, posInfo); @@ -359,18 +359,12 @@ namespace Barotrauma DebugConsole.Log("Received spawn data for " + speciesName); - string configPath = GetConfigFile(speciesName); - if (string.IsNullOrEmpty(configPath)) - { - throw new Exception("Error in character spawn data - could not find a config file for the character \"" + configPath + "\"!"); - } - Character character = null; if (noInfo) { if (!spawn) return null; - character = Create(configPath, position, seed, null, true); + character = Create(speciesName, position, seed, null, true); character.ID = id; } else @@ -383,19 +377,13 @@ namespace Barotrauma if (!spawn) return null; - string infoConfigPath = GetConfigFile(infoSpeciesName); - if (string.IsNullOrEmpty(infoConfigPath)) - { - throw new Exception("Error in character spawn data - could not find a config file for the character info \"" + configPath + "\"!"); - } + CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); - CharacterInfo info = CharacterInfo.ClientRead(infoConfigPath, inc); - - character = Create(configPath, position, seed, info, GameMain.Client.ID != ownerId, hasAi); + character = Create(infoSpeciesName, position, seed, info, GameMain.Client.ID != ownerId, hasAi); character.ID = id; character.TeamID = (TeamType)teamID; - if (configPath == HumanConfigFile && character.TeamID != TeamType.FriendlyNPC) + if (character.IsHuman && character.TeamID != TeamType.FriendlyNPC) { CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID); GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo); @@ -421,15 +409,6 @@ namespace Barotrauma return character; } - private void ReadTraitorStatus(IReadMessage msg) - { - IsTraitor = msg.ReadBoolean(); - if (IsTraitor) - { - TraitorCurrentObjective = msg.ReadString(); - } - } - private void ReadStatus(IReadMessage msg) { bool isDead = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterSound.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterSound.cs index 776525329..a096c3cc9 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterSound.cs @@ -1,6 +1,4 @@ -using System; -using System.Xml.Linq; -using Barotrauma.Sounds; +using Barotrauma.Sounds; namespace Barotrauma { @@ -12,29 +10,18 @@ namespace Barotrauma } private readonly RoundSound roundSound; + public readonly CharacterParams.SoundParams Params; - public readonly SoundType Type; + public SoundType Type => Params.State; + public Gender Gender => Params.Gender; + public float Volume => roundSound.Volume; + public float Range => roundSound.Range; + public Sound Sound => roundSound?.Sound; - public float Volume + public CharacterSound(CharacterParams.SoundParams soundParams) { - get { return roundSound.Volume; } - } - public float Range - { - get { return roundSound.Range; } - } - public Sound Sound - { - get { return roundSound?.Sound; } - } - - public readonly Gender Gender; - - public CharacterSound(XElement element) - { - roundSound = Submarine.LoadRoundSound(element); - Enum.TryParse(element.GetAttributeString("state", "Idle"), true, out Type); - Enum.TryParse(element.GetAttributeString("gender", "None"), true, out Gender); + Params = soundParams; + roundSound = Submarine.LoadRoundSound(soundParams.Element); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs index 110bc3c49..2789f2703 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs @@ -95,6 +95,8 @@ namespace Barotrauma private const float UpdateDisplayedAfflictionsInterval = 0.5f; private List currentDisplayedAfflictions = new List(); + public float DisplayedVitality, DisplayVitalityDelay; + public bool MouseOnElement { get { return highlightedLimbIndex > -1 || GUI.MouseOn == dropItemArea; } @@ -159,6 +161,8 @@ namespace Barotrauma partial void InitProjSpecific(XElement element, Character character) { + DisplayedVitality = MaxVitality; + if (strengthTexts == null) { strengthTexts = new string[] @@ -349,15 +353,17 @@ namespace Barotrauma private void OnAttacked(Character attacker, AttackResult attackResult) { - if (Math.Abs(attackResult.Damage) < 0.01f && attackResult.Afflictions.Count == 0) return; + if (Math.Abs(attackResult.Damage) < 0.01f && attackResult.Afflictions.Count == 0) { return; } DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f); - if (healthShadowDelay <= 0.0f) healthShadowDelay = 1.0f; + if (healthShadowDelay <= 0.0f) { healthShadowDelay = 1.0f; } - if (healthBarPulsateTimer <= 0.0f) healthBarPulsatePhase = 0.0f; + if (healthBarPulsateTimer <= 0.0f) { healthBarPulsatePhase = 0.0f; } healthBarPulsateTimer = 1.0f; float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality)); damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1); + + DisplayVitalityDelay = 0.5f; } private void UpdateAlignment() @@ -435,6 +441,35 @@ namespace Barotrauma uiScale = GUI.Scale; } + public void UpdateClientSpecific(float deltaTime) + { + if (GameMain.NetworkMember == null) + { + DisplayedVitality = Vitality; + } + else + { + DisplayVitalityDelay -= deltaTime; + if (DisplayVitalityDelay <= 0.0f) + { + DisplayedVitality = Vitality; + } + } + + if (damageIntensity > 0) + { + damageIntensity -= deltaTime * damageIntensityDropdownRate; + if (damageIntensity < 0) + { + damageIntensity = 0; + } + } + if (DamageOverlayTimer > 0.0f) + { + DamageOverlayTimer -= deltaTime; + } + } + partial void UpdateOxygenProjSpecific(float prevOxygen) { if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && @@ -492,20 +527,7 @@ namespace Barotrauma }); updateDisplayedAfflictionsTimer = UpdateDisplayedAfflictionsInterval; } - - if (DamageOverlayTimer > 0.0f) - { - DamageOverlayTimer -= deltaTime; - } - if (damageIntensity > 0) - { - damageIntensity -= deltaTime * damageIntensityDropdownRate; - if (damageIntensity < 0) - { - damageIntensity = 0; - } - } - + if (healthShadowDelay > 0.0f) { healthShadowDelay -= deltaTime; @@ -639,12 +661,12 @@ namespace Barotrauma } else { - healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(Vitality / MaxVitality, Color.Red, Color.Orange, Color.Green); + healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, Color.Red, Color.Orange, Color.Green); healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; healthBar.BarSize = healthWindowHealthBar.BarSize = - (Vitality > 0.0f) ? - (MaxVitality > 0.0f ? Vitality / MaxVitality : 0.0f) : - (Math.Abs(MinVitality) > 0.0f ? 1.0f - Vitality / MinVitality : 0.0f); + (DisplayedVitality > 0.0f) ? + (MaxVitality > 0.0f ? DisplayedVitality / MaxVitality : 0.0f) : + (Math.Abs(MinVitality) > 0.0f ? 1.0f - DisplayedVitality / MinVitality : 0.0f); if (healthBarPulsateTimer > 0.0f) { @@ -1446,6 +1468,9 @@ namespace Barotrauma } } } + + CalculateVitality(); + DisplayedVitality = Vitality; } partial void UpdateLimbAfflictionOverlays() diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/Source/Characters/Jobs/JobPrefab.cs index ad6632f34..886ee061b 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Jobs/JobPrefab.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System.Linq; namespace Barotrauma { @@ -29,13 +30,18 @@ namespace Barotrauma } var itemContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.5f), paddedFrame.RectTransform, Anchor.TopRight) - { RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) }); + { RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) }) + { + Stretch = true + }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), TextManager.Get("Items", fallBackTag: "mapentitycategory.equipment"), font: GUI.LargeFont); - foreach (string itemName in ItemNames) + foreach (string itemName in ItemNames.Distinct()) { + int count = ItemNames.Count(i => i == itemName); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), - " - " + itemName, font: GUI.SmallFont); + " - " + (count == 1 ? itemName : itemName + " x" + count), + font: GUI.SmallFont); } return backFrame; diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs b/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs index 082d577b7..1b0f04fa5 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs @@ -11,6 +11,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; +using SpriteParams = Barotrauma.RagdollParams.SpriteParams; +using Barotrauma.Extensions; namespace Barotrauma { @@ -18,8 +20,9 @@ namespace Barotrauma { public void UpdateDeformations(float deltaTime) { - float jointMidAngle = (LowerLimit + UpperLimit) / 2.0f; - float jointAngle = this.JointAngle - jointMidAngle; + float diff = Math.Abs(UpperLimit - LowerLimit); + float strength = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, MathHelper.Pi, diff)); + float jointAngle = this.JointAngle * strength; JointBendDeformation limbADeformation = LimbA.Deformations.Find(d => d is JointBendDeformation) as JointBendDeformation; JointBendDeformation limbBDeformation = LimbB.Deformations.Find(d => d is JointBendDeformation) as JointBendDeformation; @@ -28,7 +31,6 @@ namespace Barotrauma { UpdateBend(LimbA, limbADeformation, this.LocalAnchorA, -jointAngle); UpdateBend(LimbB, limbBDeformation, this.LocalAnchorB, jointAngle); - } void UpdateBend(Limb limb, JointBendDeformation deformation, Vector2 localAnchor, float angle) @@ -74,6 +76,14 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch) { + // TODO: move this into the character editor + //var mouthPos = ragdoll.GetMouthPosition(); + //if (mouthPos != null) + //{ + // var pos = ConvertUnits.ToDisplayUnits(mouthPos.Value); + // pos.Y = -pos.Y; + // ShapeExtensions.DrawPoint(spriteBatch, pos, Color.Red, size: 5); + //} return; // A debug visualisation on the bezier curve between limbs. var start = LimbA.WorldPosition; @@ -110,6 +120,9 @@ namespace Barotrauma public Sprite Sprite { get; protected set; } public DeformableSprite DeformSprite { get; protected set; } + + public List DecorativeSprites { get; private set; } = new List(); + public Sprite ActiveSprite { get @@ -130,32 +143,23 @@ namespace Barotrauma public WearableSprite HuskSprite { get; private set; } public WearableSprite HerpesSprite { get; private set; } - public void LoadHuskSprite() - { - var info = character.Info; - if (info == null) { return; } - var element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), WearableType.Husk).FirstOrDefault(); - if (element != null) - { - HuskSprite = new WearableSprite(element.Element("sprite"), WearableType.Husk); - } - } - public void LoadHerpesSprite() - { - var info = character.Info; - if (info == null) { return; } - var element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), WearableType.Herpes).FirstOrDefault(); - if (element != null) - { - HerpesSprite = new WearableSprite(element.Element("sprite"), WearableType.Herpes); - } - } + public void LoadHuskSprite() => HuskSprite = GetWearableSprite(WearableType.Husk); + public void LoadHerpesSprite() => HerpesSprite = GetWearableSprite(WearableType.Herpes); - public float TextureScale => limbParams.Ragdoll.TextureScale; + public float TextureScale => Params.Ragdoll.TextureScale; public Sprite DamagedSprite { get; private set; } public List ConditionalSprites { get; private set; } = new List(); + private Dictionary spriteAnimState = new Dictionary(); + private Dictionary> DecorativeSpriteGroups = new Dictionary>(); + + class SpriteState + { + public float RotationState; + public float OffsetState; + public bool IsActive = true; + } public Color InitialLightSourceColor { @@ -183,25 +187,38 @@ namespace Barotrauma set { burnOverLayStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } } - public string HitSoundTag { get; private set; } + public string HitSoundTag => Params?.Sound?.Tag; partial void InitProjSpecific(XElement element) { + for (int i = 0; i < Params.decorativeSpriteParams.Count; i++) + { + var param = Params.decorativeSpriteParams[i]; + var decorativeSprite = new DecorativeSprite(param.Element, file: GetSpritePath(param.Element, param)); + DecorativeSprites.Add(decorativeSprite); + int groupID = decorativeSprite.RandomGroupID; + if (!DecorativeSpriteGroups.ContainsKey(groupID)) + { + DecorativeSpriteGroups.Add(groupID, new List()); + } + DecorativeSpriteGroups[groupID].Add(decorativeSprite); + spriteAnimState.Add(decorativeSprite, new SpriteState()); + } foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - Sprite = new Sprite(subElement, "", GetSpritePath(subElement)); + Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams)); break; case "damagedsprite": - DamagedSprite = new Sprite(subElement, "", GetSpritePath(subElement)); + DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams)); break; case "conditionalsprite": - ConditionalSprites.Add(new ConditionalSprite(subElement, character, file: GetSpritePath(subElement))); + ConditionalSprites.Add(new ConditionalSprite(subElement, character, file: GetSpritePath(subElement, null))); break; case "deformablesprite": - DeformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement)); + DeformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams)); foreach (XElement animationElement in subElement.Elements()) { int sync = animationElement.GetAttributeInt("sync", -1); @@ -231,32 +248,63 @@ namespace Barotrauma LightSource = new LightSource(subElement); InitialLightSourceColor = LightSource.Color; break; - case "sound": - HitSoundTag = subElement.GetAttributeString("tag", ""); - if (string.IsNullOrWhiteSpace(HitSoundTag)) - { - //legacy support - HitSoundTag = subElement.GetAttributeString("file", ""); - } - break; } } } - public void RecreateSprite() + public void RecreateSprites() { - if (Sprite == null) { return; } - Sprite.Remove(); - var source = Sprite.SourceElement; - Sprite = new Sprite(source, file: GetSpritePath(source)); + if (Sprite != null) + { + Sprite.Remove(); + var source = Sprite.SourceElement; + Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams)); + } + if (DeformSprite != null) + { + DeformSprite.Remove(); + var source = DeformSprite.Sprite.SourceElement; + DeformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams)); + } + if (DamagedSprite != null) + { + DamagedSprite.Remove(); + var source = DamagedSprite.SourceElement; + DamagedSprite = new Sprite(source, file: GetSpritePath(source, Params.damagedSpriteParams)); + } + for (int i = 0; i < ConditionalSprites.Count; i++) + { + var conditionalSprite = ConditionalSprites[i]; + conditionalSprite.Remove(); + var source = conditionalSprite.SourceElement; + // TODO: lazy load? + ConditionalSprites[i] = new ConditionalSprite(source, character, file: GetSpritePath(source, null)); + } + for (int i = 0; i < DecorativeSprites.Count; i++) + { + var decorativeSprite = DecorativeSprites[i]; + decorativeSprite.Remove(); + var source = decorativeSprite.Sprite.SourceElement; + DecorativeSprites[i] = new DecorativeSprite(source, file: GetSpritePath(source, Params.decorativeSpriteParams[i])); + } + } + + private string GetSpritePath(XElement element, SpriteParams spriteParams) + { + string texturePath = element.GetAttributeString("texture", null); + if (string.IsNullOrWhiteSpace(texturePath) && spriteParams != null) + { + texturePath = spriteParams.Ragdoll.Texture; + } + return GetSpritePath(texturePath); } /// /// Get the full path of a limb sprite, taking into account tags, gender and head id /// - private string GetSpritePath(XElement element) + private string GetSpritePath(string texturePath) { - string spritePath = element.Attribute("texture")?.Value ?? ""; + string spritePath = texturePath; string spritePathWithTags = spritePath; if (character.Info != null && character.IsHumanoid) { @@ -274,16 +322,19 @@ namespace Barotrauma Path.GetFileNameWithoutExtension(spritePath) + tags + Path.GetExtension(spritePath)); } } - return File.Exists(spritePathWithTags) ? spritePathWithTags : spritePath; } partial void LoadParamsProjSpecific() { bool isFlipped = dir == Direction.Left; - Sprite?.LoadParams(limbParams.normalSpriteParams, isFlipped); - DamagedSprite?.LoadParams(limbParams.damagedSpriteParams, isFlipped); - DeformSprite?.Sprite.LoadParams(limbParams.deformSpriteParams, isFlipped); + Sprite?.LoadParams(Params.normalSpriteParams, isFlipped); + DamagedSprite?.LoadParams(Params.damagedSpriteParams, isFlipped); + DeformSprite?.Sprite.LoadParams(Params.deformSpriteParams, isFlipped); + for (int i = 0; i < DecorativeSprites.Count; i++) + { + DecorativeSprites[i].Sprite?.LoadParams(Params.decorativeSpriteParams[i], isFlipped); + } } partial void AddDamageProjSpecific(Vector2 simPosition, List afflictions, bool playSound, List appliedDamageModifiers) @@ -378,6 +429,8 @@ namespace Barotrauma LightSource.ParentSub = body.Submarine; LightSource.Rotation = (dir == Direction.Right) ? body.Rotation : body.Rotation - MathHelper.Pi; } + + UpdateSpriteStates(deltaTime); } public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null) @@ -401,7 +454,14 @@ namespace Barotrauma body.Dir = Dir; - bool hideLimb = wearingItems.Any(w => w != null && w.HideLimb); + bool enableHuskSprite = character.IsHusk || character.CharacterHealth.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; + float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + + bool hideLimb = Params.Hide || + enableHuskSprite && HuskSprite != null && HuskSprite.HideLimb || + OtherWearables.Any(w => w.HideLimb) || + wearingItems.Any(w => w != null && w.HideLimb); + // TODO: there's now two calls to this, because body.Draw() method calls this too -> is this an issue? body.UpdateDrawPosition(); if (!hideLimb) @@ -418,44 +478,68 @@ namespace Barotrauma { DeformSprite.Reset(); } - body.Draw(DeformSprite, cam, Vector2.One * Scale * TextureScale, color); + body.Draw(DeformSprite, cam, Vector2.One * Scale * TextureScale, color, Params.MirrorHorizontally); } else { - body.Draw(spriteBatch, activeSprite, color, null, Scale * TextureScale); + body.Draw(spriteBatch, activeSprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); } } - + SpriteEffects spriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; if (LightSource != null) { LightSource.Position = body.DrawPosition; LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; } + if (damageOverlayStrength > 0.0f && DamagedSprite != null && !hideLimb) + { + DamagedSprite.Draw(spriteBatch, + new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), + color * Math.Min(damageOverlayStrength, 1.0f), ActiveSprite.Origin, + -body.DrawRotation, + Scale, spriteEffect, ActiveSprite.Depth - 0.0000015f); + } + foreach (var decorativeSprite in DecorativeSprites) + { + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + var ca = (float)Math.Cos(-body.Rotation); + var sa = (float)Math.Sin(-body.Rotation); + Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), color, + -body.Rotation + rotation, Scale, spriteEffect, + depth: decorativeSprite.Sprite.Depth); + } float depthStep = 0.000001f; + float step = depthStep; WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); - SpriteEffects spriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; + if (Params.MirrorHorizontally) + { + spriteEffect = spriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; + } + if (Params.MirrorVertically) + { + spriteEffect |= SpriteEffects.FlipVertically; + } if (onlyDrawable == null) { if (HerpesSprite != null) { - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - if (herpesStrength > 0.0f) - { - DrawWearable(HerpesSprite, depthStep, spriteBatch, color * Math.Min(herpesStrength / 10.0f, 1.0f), spriteEffect); - depthStep += 0.000001f; - } + DrawWearable(HerpesSprite, depthStep, spriteBatch, color * Math.Min(herpesStrength / 10.0f, 1.0f), spriteEffect); + depthStep += step; } - if (HuskSprite != null && (character.SpeciesName == "Humanhusk" || (character.SpeciesName == "Human" && - character.CharacterHealth.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active))) + if (HuskSprite != null && enableHuskSprite) { DrawWearable(HuskSprite, depthStep, spriteBatch, color, spriteEffect); - depthStep += 0.000001f; + depthStep += step; } foreach (WearableSprite wearable in OtherWearables) { + if (wearable.Type == WearableType.Beard && enableHuskSprite && HuskSprite != null) { continue; } DrawWearable(wearable, depthStep, spriteBatch, color, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front - depthStep += 0.000001f; + depthStep += step; } } foreach (WearableSprite wearable in WearingItems) @@ -463,18 +547,7 @@ namespace Barotrauma if (onlyDrawable != null && onlyDrawable != wearable) continue; DrawWearable(wearable, depthStep, spriteBatch, color, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front - depthStep += 0.000001f; - } - - if (damageOverlayStrength > 0.0f && DamagedSprite != null && !hideLimb) - { - float depth = ActiveSprite.Depth - 0.0000015f; - - DamagedSprite.Draw(spriteBatch, - new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), - color * Math.Min(damageOverlayStrength, 1.0f), ActiveSprite.Origin, - -body.DrawRotation, - 1.0f, spriteEffect, depth); + depthStep += step; } if (GameMain.DebugDraw) @@ -492,7 +565,7 @@ namespace Barotrauma from.Y = -from.Y; Vector2 to = ConvertUnits.ToDisplayUnits(attachJoint.WorldAnchorB); to.Y = -to.Y; - var localFront = body.GetLocalFront(MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation)); + var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); var front = ConvertUnits.ToDisplayUnits(body.FarseerBody.GetWorldPoint(localFront)); front.Y = -front.Y; GUI.DrawLine(spriteBatch, bodyDrawPos, front, Color.Yellow, width: 2); @@ -503,25 +576,89 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 10, 10), Color.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(ragdoll.RagdollParams.SpritesheetOrientation)))); + //Vector2 mainLimbFront = ConvertUnits.ToDisplayUnits(ragdoll.MainLimb.body.FarseerBody.GetWorldPoint(ragdoll.MainLimb.body.GetFrontLocal(MathHelper.ToRadians(limbParams.Orientation)))); //mainLimbFront.Y = -mainLimbFront.Y; //var mainLimbDrawPos = ragdoll.MainLimb.body.DrawPosition; //mainLimbDrawPos.Y = -mainLimbDrawPos.Y; //GUI.DrawLine(spriteBatch, mainLimbDrawPos, mainLimbFront, Color.White, width: 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)mainLimbFront.X, (int)mainLimbFront.Y, 10, 10), Color.Yellow, true); } - foreach (var modifier in damageModifiers) + DrawDamageModifiers(spriteBatch, cam, bodyDrawPos, isScreenSpace: false); + } + } + + private void UpdateSpriteStates(float deltaTime) + { + foreach (int spriteGroup in DecorativeSpriteGroups.Keys) + { + for (int i = 0; i < DecorativeSpriteGroups[spriteGroup].Count; i++) { - float rotation = -body.TransformedRotation + GetArmorSectorRotationOffset(modifier.ArmorSector) * Dir; - Vector2 forward = VectorExtensions.Forward(rotation); - float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2); - color = modifier.DamageMultiplier > 1 ? Color.Red : Color.GreenYellow; - GUI.DrawLine(spriteBatch, bodyDrawPos, bodyDrawPos + Vector2.Normalize(forward) * size, color, width: (int)Math.Round(4 / cam.Zoom)); - ShapeExtensions.DrawSector(spriteBatch, bodyDrawPos, size, GetArmorSectorSize(modifier.ArmorSector) * Dir, 40, color, rotation + MathHelper.Pi, thickness: 2 / cam.Zoom); + var decorativeSprite = DecorativeSpriteGroups[spriteGroup][i]; + if (decorativeSprite == null) { continue; } + if (spriteGroup > 0) + { + // TODO + //int activeSpriteIndex = ID % DecorativeSpriteGroups[spriteGroup].Count; + //if (i != activeSpriteIndex) + //{ + // spriteAnimState[decorativeSprite].IsActive = false; + // continue; + //} + } + + //check if the sprite is active (whether it should be drawn or not) + var spriteState = spriteAnimState[decorativeSprite]; + spriteState.IsActive = true; + foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) + { + if (!conditional.Matches(this)) + { + spriteState.IsActive = false; + break; + } + } + if (!spriteState.IsActive) { continue; } + + //check if the sprite should be animated + bool animate = true; + foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) + { + if (!conditional.Matches(this)) { animate = false; break; } + } + if (!animate) { continue; } + spriteState.OffsetState += deltaTime; + spriteState.RotationState += deltaTime; } } } + public void DrawDamageModifiers(SpriteBatch spriteBatch, Camera cam, Vector2 startPos, bool isScreenSpace) + { + foreach (var modifier in damageModifiers) + { + float rotation = -body.TransformedRotation + GetArmorSectorRotationOffset(modifier.ArmorSectorInRadians) * Dir; + Vector2 forward = VectorExtensions.Forward(rotation); + float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2); + if (isScreenSpace) + { + size *= cam.Zoom; + } + Color color = modifier.DamageMultiplier > 1 ? Color.Red : Color.GreenYellow; + int width = 4; + if (!isScreenSpace) + { + width = (int)Math.Round(width / cam.Zoom); + } + GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(forward) * size, color, width: width); + int thickness = 2; + if (!isScreenSpace) + { + thickness = (int)Math.Round(thickness / cam.Zoom); + } + ShapeExtensions.DrawSector(spriteBatch, startPos, size, GetArmorSectorSize(modifier.ArmorSectorInRadians) * Dir, 40, color, rotation + MathHelper.Pi, thickness); + } + } + private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, SpriteEffects spriteEffect) { if (wearable.InheritSourceRect) @@ -584,6 +721,26 @@ namespace Barotrauma Scale * textureScale, spriteEffect, depth); } + private WearableSprite GetWearableSprite(WearableType type, bool random = false) + { + var info = character.Info; + if (info == null) { return null; } + XElement element; + if (random) + { + element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.FirstOrDefault(); + } + else + { + element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.GetRandom(Rand.RandSync.ClientOnly); + } + if (element != null) + { + return new WearableSprite(element.Element("sprite"), type); + } + return null; + } + partial void RemoveProjSpecific() { Sprite?.Remove(); @@ -595,6 +752,9 @@ namespace Barotrauma DeformSprite?.Sprite?.Remove(); DeformSprite = null; + DecorativeSprites.ForEach(s => s.Remove()); + ConditionalSprites.Clear(); + ConditionalSprites.ForEach(s => s.Remove()); ConditionalSprites.Clear(); diff --git a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs index 2d6c93afe..81c38ada9 100644 --- a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs @@ -49,6 +49,7 @@ namespace Barotrauma private static bool isOpen; public static bool IsOpen => isOpen; + public static bool Paused = false; private static GUITextBlock activeQuestionText; @@ -773,9 +774,15 @@ namespace Barotrauma } }, isCheat: true)); - commands.Add(new Command("messagebox", "", (string[] args) => + commands.Add(new Command("messagebox|guimessagebox", "messagebox [header] [msg] [default/ingame]: Creates a message box.", (string[] args) => { - new GUIMessageBox("", string.Join(" ", args)); + var msgBox = new GUIMessageBox( + args.Length > 0 ? args[0] : "", + args.Length > 1 ? args[1] : "", + buttons: new string[] { "OK" }, + type: args.Length < 3 || args[2] == "default" ? GUIMessageBox.Type.Default : GUIMessageBox.Type.InGame); + + msgBox.Buttons[0].OnClicked = msgBox.Close; })); AssignOnExecute("debugdraw", (string[] args) => @@ -1121,7 +1128,7 @@ namespace Barotrauma { string filePath = args.Length > 0 ? args[0] : "Content/Texts/EnglishVanilla.xml"; var doc = XMLExtensions.TryLoadXml(filePath); - if (doc?.Root == null) return; + if (doc == null) { return; } List lines = new List(); foreach (XElement element in doc.Root.Elements()) { @@ -1154,6 +1161,7 @@ namespace Barotrauma return; } var doc = XMLExtensions.TryLoadXml(destinationPath); + if (doc == null) { return; } int i = 0; foreach (XElement element in doc.Root.Elements()) { @@ -1186,6 +1194,8 @@ namespace Barotrauma var sourceDoc = XMLExtensions.TryLoadXml(sourcePath); var destinationDoc = XMLExtensions.TryLoadXml(destinationPath); + if (sourceDoc == null || destinationDoc == null) { return; } + XElement destinationElement = destinationDoc.Root.Elements().First(); foreach (XElement element in sourceDoc.Root.Elements()) { @@ -1221,6 +1231,76 @@ namespace Barotrauma } File.WriteAllLines(filePath, lines); })); + + + commands.Add(new Command("itemcomponentdocumentation", "", (string[] args) => + { + Dictionary typeNames = new Dictionary + { + { "Single", "float"}, + { "Int32", "integer"}, + { "Boolean", "true/false"}, + { "String", "text"}, + }; + + var itemComponentTypes = typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))); + string filePath = args.Length > 0 ? args[0] : "ItemComponentDocumentation.txt"; + List lines = new List(); + foreach (Type t in itemComponentTypes) + { + lines.Add($"[b]{t.Name}[/b]"); + lines.Add(""); + + var properties = t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly);//.Cast(); + Dictionary dictionary = new Dictionary(); + foreach (var property in properties) + { + object[] attributes = property.GetCustomAttributes(true); + Serialize serialize = attributes.FirstOrDefault(a => a is Serialize) as Serialize; + if (serialize == null) { continue; } + + string propertyTypeName = property.PropertyType.Name; + if (typeNames.ContainsKey(propertyTypeName)) + { + propertyTypeName = typeNames[propertyTypeName]; + } + else if (property.PropertyType.IsEnum) + { + List valueNames = new List(); + foreach (object enumValue in Enum.GetValues(property.PropertyType)) + { + valueNames.Add(enumValue.ToString()); + } + propertyTypeName = string.Join("/", valueNames); + } + + lines.Add($"{property.Name} ({propertyTypeName})"); + + if (!string.IsNullOrEmpty(serialize.Description)) + { + lines.Add(serialize.Description); + } + Editable editable = attributes.FirstOrDefault(a => a is Editable) as Editable; + if (editable != null) + { + if (editable.MinValueFloat > float.MinValue || editable.MaxValueFloat < float.MaxValue) + { + lines.Add("Range: " + editable.MinValueFloat+"-"+editable.MaxValueFloat); + } + else if (editable.MinValueInt > int.MinValue || editable.MaxValueInt < int.MaxValue) + { + lines.Add("Range: " + editable.MinValueInt + "-" + editable.MaxValueInt); + } + } + + lines.Add("Default value: " + serialize.defaultValue); + lines.Add(""); + } + lines.Add(""); + } + File.WriteAllLines(filePath, lines); + System.Diagnostics.Process.Start(Path.GetFullPath(filePath)); + })); #if DEBUG commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => { @@ -1239,12 +1319,6 @@ namespace Barotrauma if (args.Length == 0) return; LocalizationCSVtoXML.Convert(args[0]); })); - - commands.Add(new Command("guimessagebox", "guimessagebox [msg] -> Creates a message box with the parameter as a message.", (string[] args) => - { - if (args.Length == 0) return; - var dialog = new GUIMessageBox("Message box", args[0]); - })); #endif commands.Add(new Command("cleanbuild", "", (string[] args) => @@ -1257,7 +1331,7 @@ namespace Barotrauma GameMain.Config.GraphicsWidth = 0; GameMain.Config.GraphicsHeight = 0; - GameMain.Config.WindowMode = WindowMode.Fullscreen; + GameMain.Config.WindowMode = WindowMode.BorderlessWindowed; NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green); NewMessage("Fullscreen enabled", Color.Green); @@ -1652,7 +1726,7 @@ namespace Barotrauma ThrowError("Not controlling any character!"); return; } - character.AnimController.ResetRagdoll(); + character.AnimController.ResetRagdoll(forceReload: true); }, isCheat: true)); commands.Add(new Command("reloadwearables", "Reloads the sprites of all limbs and wearable sprites (clothing) of the controlled character. Provide id or name if you want to target another character.", args => @@ -1762,7 +1836,7 @@ namespace Barotrauma { if (limb.type != LimbType.Head) { - limb.RecreateSprite(); + limb.RecreateSprites(); } foreach (var wearable in limb.WearingItems) { @@ -1846,6 +1920,19 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace); } }, isCheat: true)); + + commands.Add(new Command("pause", "Toggles the pause state when playing offline", (string[] args) => + { + if (GameMain.NetworkMember == null) + { + Paused = !Paused; + DebugConsole.NewMessage("Game paused: " + Paused); + } + else + { + DebugConsole.NewMessage("Cannot pause when a multiplayer session is active."); + } + })); } private static void ReloadWearables(Character character, int variant = 0) diff --git a/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs index 1ca52a559..907f11bb5 100644 --- a/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs @@ -1,17 +1,18 @@ -using Barotrauma.Networking; - -namespace Barotrauma +namespace Barotrauma { partial class Mission { partial void ShowMessageProjSpecific(int index) { - if (index >= Headers.Count && index >= Messages.Count) return; + if (index >= Headers.Count && index >= Messages.Count) { return; } string header = index < Headers.Count ? Headers[index] : ""; string message = index < Messages.Count ? Messages[index] : ""; - new GUIMessageBox(header, message); + new GUIMessageBox(header, message, buttons: new string[0], type: GUIMessageBox.Type.InGame, icon: Prefab.Icon) + { + IconColor = Prefab.IconColor + }; } } } diff --git a/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs index 759e34247..788849362 100644 --- a/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs @@ -9,8 +9,9 @@ namespace Barotrauma { if (mission == null) return; - new GUIMessageBox(mission.Name, mission.Description, new Vector2(0.25f, 0.0f), new Point(400, 200)) + new GUIMessageBox(mission.Name, mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { + IconColor = mission.Prefab.IconColor, UserData = "missionstartmessage" }; } diff --git a/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionPrefab.cs new file mode 100644 index 000000000..58d480b8a --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionPrefab.cs @@ -0,0 +1,33 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class MissionPrefab + { + public Sprite Icon + { + get; + private set; + } + + public Color IconColor + { + get; + private set; + } + + partial void InitProjSpecific(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() != "icon") { continue; } + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/Source/Fonts/ScalableFont.cs index 3ecda8c76..ca6215f53 100644 --- a/Barotrauma/BarotraumaClient/Source/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/Source/Fonts/ScalableFont.cs @@ -251,7 +251,7 @@ namespace Barotrauma { this.texDims = texDims; this.baseChar = baseChar; - face.SetPixelSizes(0, size); + lock (mutex) { face.SetPixelSizes(0, size); } face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal); baseHeight = face.Glyph.Metrics.Height.ToInt32(); CrossThread.RequestExecutionOnMainThread(() => @@ -263,6 +263,7 @@ namespace Barotrauma uint glyphIndex = face.GetCharIndex(character); if (glyphIndex == 0) { return; } + lock (mutex) { face.SetPixelSizes(0, size); } face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs index 00cc87815..c87231d3d 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs @@ -1,3 +1,4 @@ +using Barotrauma.CharacterEditor; using Barotrauma.Extensions; using Barotrauma.Sounds; using Barotrauma.Tutorials; @@ -8,6 +9,7 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -154,19 +156,38 @@ namespace Barotrauma public static void Init(GameWindow window, IEnumerable selectedContentPackages, GraphicsDevice graphicsDevice) { GUI.graphicsDevice = graphicsDevice; - var uiStyles = ContentPackage.GetFilesOfType(selectedContentPackages, ContentType.UIStyle).ToList(); - if (uiStyles.Count == 0) + var files = ContentPackage.GetFilesOfType(selectedContentPackages, ContentType.UIStyle); + XElement selectedStyle = null; + foreach (var file in files) + { + XDocument doc = XMLExtensions.TryLoadXml(file); + 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}'", 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!"); - return; } - else if (uiStyles.Count > 1) + else { - DebugConsole.ThrowError("Multiple UI styles defined in the selected content package! Selecting the first one."); + Style = new GUIStyle(selectedStyle, graphicsDevice); } - Style = new GUIStyle(uiStyles[0], graphicsDevice); if (CJKFont == null) { CJKFont = new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", @@ -407,6 +428,10 @@ namespace Barotrauma { debugDrawEvents = !debugDrawEvents; } + if (MouseOn != null) + { + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 500, 20), $"Selected UI Element: {MouseOn.GetType().ToString()}", Color.LightGreen, Color.Black * 0.5f, 0, SmallFont); + } } if (HUDLayoutSettings.DebugDraw) HUDLayoutSettings.Draw(spriteBatch); @@ -601,10 +626,7 @@ namespace Barotrauma private static void HandlePersistingElements(float deltaTime) { - if (GUIMessageBox.VisibleBox != null && GUIMessageBox.VisibleBox.UserData as string != "verificationprompt" && GUIMessageBox.VisibleBox.UserData as string != "bugreporter") - { - GUIMessageBox.VisibleBox.AddToGUIUpdateList(); - } + GUIMessageBox.AddActiveToGUIUpdateList(); if (pauseMenuOpen) { @@ -1570,7 +1592,7 @@ namespace Barotrauma button.OnClicked += (btn, userData) => { var quitButton = button; - if (GameMain.GameSession != null || (Screen.Selected is CharacterEditorScreen charEditScreen || Screen.Selected is SubEditorScreen subEditScreen)) + if (GameMain.GameSession != null || (Screen.Selected is CharacterEditorScreen || Screen.Selected is SubEditorScreen)) { string text = GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification"; var msgBox = new GUIMessageBox("", TextManager.Get(text), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs index 0ea3828e8..79282e4bd 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs @@ -163,6 +163,12 @@ namespace Barotrauma { TextColor = this.style == null ? Color.Black : this.style.textColor }; + if (rectT.Rect.Height == 0 && !string.IsNullOrEmpty(text)) + { + RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(textBlock.Text).Y)); + RectTransform.MinSize = textBlock.RectTransform.MinSize = new Point(0, Rect.Height); + TextBlock.SetTextPos(); + } GUI.Style.Apply(textBlock, "", this); Enabled = true; } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs index 8bfa3c290..cad4abb24 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs @@ -4,6 +4,10 @@ using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using System; +using System.Xml.Linq; +using System.IO; +using RestSharp; +using System.Net; namespace Barotrauma { @@ -596,5 +600,340 @@ namespace Barotrauma this.style = style; } + + public static GUIComponent FromXML(XElement element, RectTransform parent) + { + GUIComponent component = null; + + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() == "conditional" && + !CheckConditional(subElement)) + { + return null; + } + } + + switch (element.Name.ToString().ToLowerInvariant()) + { + case "text": + case "guitextblock": + component = LoadGUITextBlock(element, parent); + break; + case "link": + component = LoadLink(element, parent); + break; + case "frame": + case "guiframe": + case "spacing": + component = LoadGUIFrame(element, parent); + break; + case "button": + case "guibutton": + component = LoadGUIButton(element, parent); + break; + case "listbox": + case "guilistbox": + component = LoadGUIListBox(element, parent); + break; + case "guilayoutgroup": + case "layoutgroup": + component = LoadGUILayoutGroup(element, parent); + break; + case "image": + case "guiimage": + component = LoadGUIImage(element, parent); + break; + case "accordion": + return LoadAccordion(element, parent); + case "gridtext": + LoadGridText(element, parent); + return null; + default: + throw new NotImplementedException("Loading GUI component \""+element.Name+"\" from XML is not implemented."); + } + + if (component != null) + { + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() == "conditional") { continue; } + FromXML(subElement, component is GUIListBox listBox ? listBox.Content.RectTransform : component.RectTransform); + } + + if (element.GetAttributeBool("resizetofitchildren", false)) + { + Vector2 relativeResizeScale = element.GetAttributeVector2("relativeresizescale", Vector2.One); + if (component is GUILayoutGroup layoutGroup) + { + layoutGroup.RectTransform.NonScaledSize = + layoutGroup.IsHorizontal ? + new Point(layoutGroup.Children.Sum(c => c.Rect.Width), layoutGroup.Rect.Height) : + component.RectTransform.MinSize = new Point(layoutGroup.Rect.Width, layoutGroup.Children.Sum(c => c.Rect.Height)); + if (layoutGroup.CountChildren > 0) + { + layoutGroup.RectTransform.NonScaledSize += + layoutGroup.IsHorizontal ? + new Point((int)((layoutGroup.CountChildren - 1) * (layoutGroup.AbsoluteSpacing + layoutGroup.Rect.Width * layoutGroup.RelativeSpacing)), 0) : + new Point(0, (int)((layoutGroup.CountChildren - 1) * (layoutGroup.AbsoluteSpacing + layoutGroup.Rect.Height * layoutGroup.RelativeSpacing))); + } + } + else if (component is GUIListBox listBox) + { + listBox.RectTransform.NonScaledSize = + listBox.ScrollBar.IsHorizontal ? + new Point(listBox.Children.Sum(c => c.Rect.Width + listBox.Spacing), listBox.Rect.Height) : + component.RectTransform.MinSize = new Point(listBox.Rect.Width, listBox.Children.Sum(c => c.Rect.Height + listBox.Spacing)); + } + else + { + component.RectTransform.NonScaledSize = + new Point( + component.Children.Max(c => c.Rect.Right) - component.Children.Min(c => c.Rect.X), + component.Children.Max(c => c.Rect.Bottom) - component.Children.Min(c => c.Rect.Y)); + } + component.RectTransform.NonScaledSize = + component.RectTransform.NonScaledSize.Multiply(relativeResizeScale); + } + } + return component; + } + + private static bool CheckConditional(XElement element) + { + foreach (XAttribute attribute in element.Attributes()) + { + switch (attribute.Name.ToString().ToLowerInvariant()) + { + case "language": + string[] languages = element.GetAttributeStringArray(attribute.Name.ToString(), new string[0]); + if (!languages.Any(l => GameMain.Config.Language.ToLower() == l.ToLower())) { return false; } + break; + case "gameversion": + var version = new Version(attribute.Value); + if (GameMain.Version != version) { return false; } + break; + case "mingameversion": + var minVersion = new Version(attribute.Value); + if (GameMain.Version < minVersion) { return false; } + break; + case "maxgameversion": + var maxVersion = new Version(attribute.Value); + if (GameMain.Version > maxVersion) { return false; } + break; + } + } + + return true; + } + + private static GUITextBlock LoadGUITextBlock(XElement element, RectTransform parent, string overrideText = null, Anchor? anchor = null) + { + string text = element.Attribute("text") == null ? + element.ElementInnerText() : + element.GetAttributeString("text", ""); + text = text.Replace(@"\n", "\n"); + + string style = element.GetAttributeString("style", ""); + if (style == "null") { style = null; } + Color? color = null; + 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()) + { + case "font": + font = GUI.Font; + break; + case "smallfont": + font = GUI.SmallFont; + break; + case "largefont": + font = GUI.LargeFont; + break; + case "videotitlefont": + font = GUI.VideoTitleFont; + break; + case "objectivetitlefont": + font = GUI.ObjectiveTitleFont; + break; + case "objectivenamefont": + font = GUI.ObjectiveNameFont; + break; + } + + var textBlock = new GUITextBlock(RectTransform.Load(element, parent), + text, color, font, alignment, wrap: wrap, style: style) + { + TextScale = scale + }; + if (anchor.HasValue) { textBlock.RectTransform.SetPosition(anchor.Value); } + textBlock.RectTransform.IsFixedSize = true; + textBlock.RectTransform.NonScaledSize = new Point(textBlock.Rect.Width, textBlock.Rect.Height); + return textBlock; + } + + private static GUIButton LoadLink(XElement element, RectTransform parent) + { + var button = LoadGUIButton(element, parent); + string url = element.GetAttributeString("url", ""); + button.OnClicked = (btn, userdata) => + { + try + { + System.Diagnostics.Process.Start(url); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to open url \""+url+"\".", e); + } + return true; + }; + return button; + } + + private static void LoadGridText(XElement element, RectTransform parent) + { + string text = element.Attribute("text") == null ? + element.ElementInnerText() : + element.GetAttributeString("text", ""); + text = text.Replace(@"\n", "\n"); + + string[] elements = text.Split(','); + RectTransform lineContainer = null; + for (int i = 0; i < elements.Length; i++) + { + switch (i % 3) + { + case 0: + lineContainer = LoadGUITextBlock(element, parent, elements[i], Anchor.CenterLeft).RectTransform; + lineContainer.Anchor = Anchor.TopCenter; + lineContainer.Pivot = Pivot.TopCenter; + lineContainer.NonScaledSize = new Point((int)(parent.NonScaledSize.X * 0.7f), lineContainer.NonScaledSize.Y); + break; + case 1: + LoadGUITextBlock(element, lineContainer, elements[i], Anchor.Center).TextAlignment = Alignment.Center; + break; + case 2: + LoadGUITextBlock(element, lineContainer, elements[i], Anchor.CenterRight).TextAlignment = Alignment.CenterRight; + break; + } + } + } + + private static GUIFrame LoadGUIFrame(XElement element, RectTransform parent) + { + string style = element.GetAttributeString("style", element.Name.ToString().ToLowerInvariant() == "spacing" ? null : ""); + if (style == "null") { style = null; } + return new GUIFrame(RectTransform.Load(element, parent), style: style); + } + + private static GUIButton LoadGUIButton(XElement element, RectTransform parent) + { + string style = element.GetAttributeString("style", ""); + if (style == "null") { style = null; } + + Alignment textAlignment = Alignment.Center; + Enum.TryParse(element.GetAttributeString("textalignment", "Center"), out textAlignment); + + string text = element.Attribute("text") == null ? + element.ElementInnerText() : + element.GetAttributeString("text", ""); + text = text.Replace(@"\n", "\n"); + + return new GUIButton(RectTransform.Load(element, parent), + text: text, + textAlignment: textAlignment, + style: style); + } + + private static GUIListBox LoadGUIListBox(XElement element, RectTransform parent) + { + string style = element.GetAttributeString("style", ""); + if (style == "null") { style = null; } + bool isHorizontal = element.GetAttributeBool("ishorizontal", !element.GetAttributeBool("isvertical", true)); + return new GUIListBox(RectTransform.Load(element, parent), isHorizontal, style: style); + } + + private static GUILayoutGroup LoadGUILayoutGroup(XElement element, RectTransform parent) + { + bool isHorizontal = element.GetAttributeBool("ishorizontal", !element.GetAttributeBool("isvertical", true)); + + Enum.TryParse(element.GetAttributeString("childanchor", "TopLeft"), out Anchor childAnchor); + return new GUILayoutGroup(RectTransform.Load(element, parent), isHorizontal, childAnchor) + { + Stretch = element.GetAttributeBool("stretch", false), + RelativeSpacing = element.GetAttributeFloat("relativespacing", 0.0f), + AbsoluteSpacing = element.GetAttributeInt("absolutespacing", 0), + }; + } + + private static GUIImage LoadGUIImage(XElement element, RectTransform parent) + { + Sprite sprite = null; + + string url = element.GetAttributeString("url", ""); + if (!string.IsNullOrEmpty(url)) + { + string localFileName = Path.GetFileNameWithoutExtension(url.Replace("/", "").Replace(":", "").Replace("https", "").Replace("http", "")) + .Replace(".", ""); + localFileName += Path.GetExtension(url); + string localFilePath = Path.Combine("Downloads", localFileName); + if (!File.Exists(localFilePath)) + { + Uri baseAddress = new Uri(url); + Uri remoteDirectory = new Uri(baseAddress, "."); + string remoteFileName = Path.GetFileName(baseAddress.LocalPath); + IRestClient client = new RestClient(remoteDirectory); + var response = client.Execute(new RestRequest(remoteFileName, Method.GET)); + if (response.ResponseStatus != ResponseStatus.Completed) { return null; } + if (response.StatusCode != HttpStatusCode.OK) { return null; } + + if (!Directory.Exists("Downloads")) { Directory.CreateDirectory("Downloads"); } + File.WriteAllBytes(localFilePath, response.RawBytes); + } + sprite = new Sprite(element, "Downloads", localFileName); + } + else + { + sprite = new Sprite(element); + } + + return new GUIImage(RectTransform.Load(element, parent), sprite, scaleToFit: true); + } + + private static GUIButton LoadAccordion(XElement element, RectTransform parent) + { + var button = LoadGUIButton(element, parent); + List content = new List(); + foreach (XElement subElement in element.Elements()) + { + var contentElement = FromXML(subElement, parent); + if (contentElement != null) + { + contentElement.Visible = false; + contentElement.IgnoreLayoutGroups = true; + content.Add(contentElement); + } + } + button.OnClicked = (btn, userdata) => + { + bool visible = content.FirstOrDefault()?.Visible ?? true; + foreach (GUIComponent contentElement in content) + { + contentElement.Visible = !visible; + contentElement.IgnoreLayoutGroups = !contentElement.Visible; + } + if (button.Parent is GUILayoutGroup layoutGroup) + { + layoutGroup.Recalculate(); + } + return true; + }; + return button; + } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs index 99b5bbc14..4cc156434 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs @@ -14,6 +14,12 @@ namespace Barotrauma get { return Math.Max(400, 400 * (GameMain.GraphicsWidth / 1920)); } } + public enum Type + { + Default, + InGame + } + public List Buttons { get; private set; } = new List(); //public GUIFrame BackgroundFrame { get; private set; } public GUILayoutGroup Content { get; private set; } @@ -22,6 +28,29 @@ namespace Barotrauma public GUITextBlock Text { get; private set; } public string Tag { get; private set; } + public GUIImage Icon + { + get; + private set; + } + + public Color IconColor + { + get { return Icon == null ? Color.White : Icon.Color; } + set + { + if (Icon == null) { return; } + Icon.Color = value; + } + } + + private bool alwaysVisible; + + private float openState; + private bool closing; + + private Type type; + public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); public GUIMessageBox(string headerText, string text, Vector2? relativeSize = null, Point? minSize = null) @@ -29,12 +58,11 @@ namespace Barotrauma { this.Buttons[0].OnClicked = Close; } - - public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, string tag = "") - : base(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "") + + 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) + : base(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { - //int width = (int)(DefaultWidth * GUI.Scale), height = 0; - int width = DefaultWidth, height = 0; + int width = (int)(DefaultWidth * (type == Type.Default ? 1.0f : 1.5f)), height = 0; if (relativeSize.HasValue) { width = (int)(GameMain.GraphicsWidth * relativeSize.Value.X); @@ -49,137 +77,198 @@ namespace Barotrauma } } - InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, Anchor.Center) { IsFixedSize = false }, style: null); + InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, type == Type.InGame ? Anchor.TopCenter : Anchor.Center) { IsFixedSize = false }, style: null); GUI.Style.Apply(InnerFrame, "", this); - - Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; + this.type = type; Tag = tag; - - Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), - headerText, textAlignment: Alignment.Center, wrap: true); - GUI.Style.Apply(Header, "", this); - Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - if (!string.IsNullOrWhiteSpace(text)) + if (type == Type.Default) { - Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), - text, textAlignment: textAlignment, wrap: true); - GUI.Style.Apply(Text, "", this); - Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = - new Point(Text.Rect.Width, Text.Rect.Height); - Text.RectTransform.IsFixedSize = true; - } + 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, textAlignment: Alignment.Center, wrap: true); + GUI.Style.Apply(Header, "", this); + Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), Content.RectTransform, Anchor.BottomCenter, maxSize: new Point(1000, 50)), - isHorizontal: true, childAnchor: buttons.Length > 1 ? Anchor.BottomLeft : Anchor.Center) - { - AbsoluteSpacing = 5, - IgnoreLayoutGroups = true - }; - buttonContainer.RectTransform.NonScaledSize = buttonContainer.RectTransform.MinSize = buttonContainer.RectTransform.MaxSize = - new Point(buttonContainer.Rect.Width, (int)(30 * GUI.Scale)); - buttonContainer.RectTransform.IsFixedSize = true; - - if (height == 0) - { - height += Header.Rect.Height + Content.AbsoluteSpacing; - height += (Text == null ? 0 : Text.Rect.Height) + Content.AbsoluteSpacing; - height += buttonContainer.Rect.Height; - if (minSize.HasValue) + if (!string.IsNullOrWhiteSpace(text)) { - height = Math.Max(height, minSize.Value.Y); + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + GUI.Style.Apply(Text, "", this); + Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = + new Point(Text.Rect.Width, Text.Rect.Height); + Text.RectTransform.IsFixedSize = true; } - InnerFrame.RectTransform.NonScaledSize = - new Point(InnerFrame.Rect.Width, (int)Math.Max(height / Content.RectTransform.RelativeSize.Y, height + (int)(50 * GUI.yScale))); - Content.RectTransform.NonScaledSize = - new Point(Content.Rect.Width, height); - } + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), Content.RectTransform, Anchor.BottomCenter, maxSize: new Point(1000, 50)), + isHorizontal: true, childAnchor: buttons.Length > 1 ? Anchor.BottomLeft : Anchor.Center) + { + AbsoluteSpacing = 5, + IgnoreLayoutGroups = true + }; + buttonContainer.RectTransform.NonScaledSize = buttonContainer.RectTransform.MinSize = buttonContainer.RectTransform.MaxSize = + new Point(buttonContainer.Rect.Width, (int)(30 * GUI.Scale)); + buttonContainer.RectTransform.IsFixedSize = true; - Buttons = new List(buttons.Length); - for (int i = 0; i < buttons.Length; i++) + if (height == 0) + { + height += Header.Rect.Height + Content.AbsoluteSpacing; + height += (Text == null ? 0 : Text.Rect.Height) + Content.AbsoluteSpacing; + height += buttonContainer.Rect.Height; + if (minSize.HasValue) { height = Math.Max(height, minSize.Value.Y); } + + InnerFrame.RectTransform.NonScaledSize = + new Point(InnerFrame.Rect.Width, (int)Math.Max(height / Content.RectTransform.RelativeSize.Y, height + (int)(50 * GUI.yScale))); + Content.RectTransform.NonScaledSize = + new Point(Content.Rect.Width, height); + } + + Buttons = new List(buttons.Length); + for (int i = 0; i < buttons.Length; i++) + { + var button = new GUIButton(new RectTransform(new Vector2(Math.Min(0.9f / buttons.Length, 0.5f), 1.0f), buttonContainer.RectTransform), buttons[i], style: "GUIButtonLarge"); + Buttons.Add(button); + } + } + else if (type == Type.InGame) { - var button = new GUIButton(new RectTransform(new Vector2(Math.Min(0.9f / buttons.Length, 0.5f), 1.0f), buttonContainer.RectTransform), buttons[i], style: "GUIButtonLarge"); - Buttons.Add(button); - } + InnerFrame.RectTransform.AbsoluteOffset = new Point(0, GameMain.GraphicsHeight); + alwaysVisible = true; + CanBeFocused = false; + GUI.Style.Apply(InnerFrame, "", this); + var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), InnerFrame.RectTransform, Anchor.Center), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + if (icon != null) + { + Icon = new GUIImage(new RectTransform(new Vector2(0.2f, 0.95f), horizontalLayoutGroup.RectTransform), icon, scaleToFit: true); + } + + Content = new GUILayoutGroup(new RectTransform(new Vector2(icon != null ? 0.65f : 0.85f, 1.0f), horizontalLayoutGroup.RectTransform)); + + var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.15f, 1.0f), horizontalLayoutGroup.RectTransform), style: null); + Buttons = new List(1) + { + new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), buttonContainer.RectTransform, Anchor.Center), + style: GUI.Style.GetComponentStyle("GUIButtonSolidHorizontalArrow") != null ? "GUIButtonSolidHorizontalArrow" : "GUIButtonHorizontalArrow") + { + OnClicked = Close + } + }; + + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + GUI.Style.Apply(Header, "", this); + Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); + + if (!string.IsNullOrWhiteSpace(text)) + { + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + GUI.Style.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 (height == 0) + { + height += Header.Rect.Height + Content.AbsoluteSpacing; + height += (Text == null ? 0 : Text.Rect.Height) + Content.AbsoluteSpacing; + if (minSize.HasValue) { height = Math.Max(height, minSize.Value.Y); } + + InnerFrame.RectTransform.NonScaledSize = + new Point(InnerFrame.Rect.Width, (int)Math.Max(height / Content.RectTransform.RelativeSize.Y, height + (int)(50 * GUI.yScale))); + Content.RectTransform.NonScaledSize = + new Point(Content.Rect.Width, height); + } + Buttons[0].RectTransform.MaxSize = new Point(Math.Min(Buttons[0].Rect.Width, Buttons[0].Rect.Height)); + } + MessageBoxes.Add(this); } - ///// - ///// This is the new constructor. - ///// TODO: for some reason the background does not prohibit input on the elements that are behind the box - ///// TODO: allow providing buttons in the constructor - ///// - /*public GUIMessageBox(RectTransform rectT, string headerText, string text, Alignment textAlignment = Alignment.TopCenter) - : base(rectT, "") + public static void AddActiveToGUIUpdateList() { - //BackgroundFrame = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), rectT, Anchor.Center), null, Color.Black * 0.5f); - float headerHeight = 0.2f; - float margin = 0.05f; - InnerFrame = new GUIFrame(rectT); - GUI.Style.Apply(InnerFrame, "", this); - Header = null; - if (!string.IsNullOrWhiteSpace(headerText)) + for (int i = 0; i < MessageBoxes.Count; i++) { - Header = new GUITextBlock(new RectTransform(new Vector2(1, headerHeight), InnerFrame.RectTransform, Anchor.TopCenter) + if (MessageBoxes[i] is GUIMessageBox alwaysVisibleMsgBox && alwaysVisibleMsgBox.alwaysVisible) { - RelativeOffset = new Vector2(0, margin) - }, headerText, textAlignment: Alignment.Center); - GUI.Style.Apply(Header, "", this); + alwaysVisibleMsgBox.AddToGUIUpdateList(); + break; + } } - if (!string.IsNullOrWhiteSpace(text)) + for (int i = MessageBoxes.Count - 1; i >= 0; i--) { - float offset = headerHeight + margin; - var size = Header == null ? Vector2.One : new Vector2(1 - margin * 2, 1 - offset + margin); - Text = new GUITextBlock(new RectTransform(size, InnerFrame.RectTransform, Anchor.TopCenter) + if (MessageBoxes[i].UserData as string == "verificationprompt" || + MessageBoxes[i].UserData as string == "bugreporter") { - RelativeOffset = new Vector2(0, offset) - }, text, textAlignment: textAlignment, wrap: true); - GUI.Style.Apply(Text, "", this); + continue; + } + if (!(MessageBoxes[i] is GUIMessageBox msgBox) || !msgBox.alwaysVisible) + { + MessageBoxes[i].AddToGUIUpdateList(); + break; + } } - MessageBoxes.Add(this); - }*/ + } + + protected override void Update(float deltaTime) + { + if (type == Type.InGame) + { + Vector2 initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); + Vector2 defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); + Vector2 endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); + + /*for (int i = MessageBoxes.IndexOf(this); i >= 0; i--) + { + if (MessageBoxes[i] is GUIMessageBox otherMsgBox && otherMsgBox != this && otherMsgBox.type == type && !otherMsgBox.closing) + { + defaultPos = new Vector2( + Math.Max(otherMsgBox.InnerFrame.RectTransform.AbsoluteOffset.X + 10 * GUI.Scale, defaultPos.X), + Math.Max(otherMsgBox.InnerFrame.RectTransform.AbsoluteOffset.Y + 10 * GUI.Scale, defaultPos.Y)); + } + }*/ + + if (!closing) + { + InnerFrame.RectTransform.AbsoluteOffset = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); + openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); + } + else + { + openState += deltaTime * 2.0f; + InnerFrame.RectTransform.AbsoluteOffset = Vector2.SmoothStep(defaultPos, endPos, openState - 1.0f).ToPoint(); + if (openState >= 2.0f) + { + if (Parent != null) { Parent.RemoveChild(this); } + if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } + } + } + } + } - //public override void AddToGUIUpdateList(bool ignoreChildren = false, bool updateLast = false) - //{ - // base.AddToGUIUpdateList(ignoreChildren, updateLast); - //} - //public override void Draw(SpriteBatch spriteBatch, bool drawChildren = true) - //{ - // if (RectTransform == null) - // { - // base.Draw(spriteBatch, drawChildren); - // } - // else - // { - // // Custom draw order so that the background is rendered behind the parent. - // if (drawChildren) - // { - // BackgroundFrame?.Draw(spriteBatch); - // } - // base.Draw(spriteBatch, false); - // if (drawChildren) - // { - // InnerFrame?.Draw(spriteBatch); - // Header?.Draw(spriteBatch); - // Text?.Draw(spriteBatch); - // Buttons.ForEach(b => b.Draw(spriteBatch)); - // } - // } - //} - public void Close() { - if (Parent != null) Parent.RemoveChild(this); - if (MessageBoxes.Contains(this)) MessageBoxes.Remove(this); + if (type == Type.InGame) + { + closing = true; + } + else + { + if (Parent != null) { Parent.RemoveChild(this); } + if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } + } } public bool Close(GUIButton button, object obj) { - Close(); - + Close(); return true; } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIStyle.cs index 24ebb083a..e01b955e6 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIStyle.cs @@ -30,27 +30,12 @@ namespace Barotrauma public SpriteSheet FocusIndicator { get; private set; } - public GUIStyle(string file, GraphicsDevice graphicsDevice) + public GUIStyle(XElement element, GraphicsDevice graphicsDevice) { this.graphicsDevice = graphicsDevice; componentStyles = new Dictionary(); - - XDocument doc; - try - { - ToolBox.IsProperFilenameCase(file); - doc = XDocument.Load(file, LoadOptions.SetBaseUri); - if (doc == null) { throw new Exception("doc is null"); } - if (doc.Root == null) { throw new Exception("doc.Root is null"); } - if (doc.Root.Elements() == null) { throw new Exception("doc.Root.Elements() is null"); } - } - catch (Exception e) - { - DebugConsole.ThrowError("Loading style \"" + file + "\" failed", e); - return; - } - configElement = doc.Root; - foreach (XElement subElement in doc.Root.Elements()) + configElement = element; + foreach (XElement subElement in configElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs index 811e8f4ea..c54a3db71 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs @@ -40,6 +40,8 @@ namespace Barotrauma private float textDepth; + private ScalableFont originalFont; + public Vector2 TextOffset { get; set; } private Vector4 padding; @@ -62,7 +64,7 @@ namespace Barotrauma set { if (base.Font == value) return; - base.Font = value; + base.Font = originalFont = value; SetTextPos(); } } @@ -74,13 +76,23 @@ namespace Barotrauma { string newText = forceUpperCase ? value?.ToUpper() : value; - if (Text == newText) return; + if (Text == newText) { return; } + //reset scale, it gets recalculated in SetTextPos - if (autoScale) textScale = 1.0f; + if (autoScale) { 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; } + } SetTextPos(); } } @@ -208,8 +220,11 @@ namespace Barotrauma //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 = font ?? GUI.Font; - if (TextManager.IsCJK(text) && !selectedFont.IsCJK) { selectedFont = GUI.CJKFont; } + var selectedFont = originalFont = font ?? GUI.Font; + if (TextManager.IsCJK(text) && !selectedFont.IsCJK) + { + selectedFont = GUI.CJKFont; + } this.Font = selectedFont; this.textAlignment = textAlignment; this.Wrap = wrap; diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs index 186be6505..94e83bef0 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs @@ -162,10 +162,11 @@ namespace Barotrauma public override ScalableFont Font { + get { return textBlock?.Font ?? base.Font; } set { base.Font = value; - if (textBlock == null) return; + if (textBlock == null) { return; } textBlock.Font = value; } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/ParamsEditor.cs b/Barotrauma/BarotraumaClient/Source/GUI/ParamsEditor.cs index 461a52fc7..2a4ce2957 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/ParamsEditor.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/ParamsEditor.cs @@ -32,7 +32,7 @@ namespace Barotrauma { rectT = rectT ?? new RectTransform(new Vector2(0.25f, 1f), GUI.Canvas) { MinSize = new Point(340, GameMain.GraphicsHeight) }; rectT.SetPosition(Anchor.TopRight); - Parent = new GUIFrame(rectT, null, new Color(20, 20, 20, 255)); + Parent = new GUIFrame(rectT, null, Color); EditorBox = new GUIListBox(new RectTransform(Vector2.One * 0.98f, rectT, Anchor.Center), color: Color.Black, style: null) { Spacing = 10 @@ -49,5 +49,7 @@ namespace Barotrauma { EditorBox = CreateEditorBox(); } + + public static Color Color = new Color(20, 20, 20, 255); } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/Source/GUI/RectTransform.cs index b2eca2968..a93cbe430 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/RectTransform.cs @@ -358,9 +358,9 @@ namespace Barotrauma parent?.ChildrenChanged?.Invoke(this); } - public static RectTransform Load(XElement element, RectTransform parent) + public static RectTransform Load(XElement element, RectTransform parent, Anchor defaultAnchor = Anchor.TopLeft) { - Enum.TryParse(element.GetAttributeString("anchor", "Center"), out Anchor anchor); + Enum.TryParse(element.GetAttributeString("anchor", defaultAnchor.ToString()), out Anchor anchor); Enum.TryParse(element.GetAttributeString("pivot", anchor.ToString()), out Pivot pivot); Point? minSize = null, maxSize = null; @@ -368,11 +368,7 @@ namespace Barotrauma //if (element.Attribute("maxsize") != null) maxSize = element.GetAttributePoint("maxsize", new Point(1000, 1000)); RectTransform rectTransform; - if (element.Attribute("relativesize") != null) - { - rectTransform = new RectTransform(element.GetAttributeVector2("relativesize", Vector2.One), parent, anchor, pivot, minSize, maxSize); - } - else + if (element.Attribute("absolutesize") != null) { rectTransform = new RectTransform(element.GetAttributePoint("absolutesize", new Point(1000, 1000)), parent, anchor, pivot) { @@ -380,6 +376,10 @@ namespace Barotrauma maxSize = maxSize }; } + else + { + rectTransform = new RectTransform(element.GetAttributeVector2("relativesize", Vector2.One), parent, anchor, pivot, minSize, maxSize); + } rectTransform.RelativeOffset = element.GetAttributeVector2("relativeoffset", Vector2.Zero); rectTransform.AbsoluteOffset = element.GetAttributePoint("absoluteoffset", Point.Zero); return rectTransform; diff --git a/Barotrauma/BarotraumaClient/Source/GameMain.cs b/Barotrauma/BarotraumaClient/Source/GameMain.cs index 655e8f95f..1199a63eb 100644 --- a/Barotrauma/BarotraumaClient/Source/GameMain.cs +++ b/Barotrauma/BarotraumaClient/Source/GameMain.cs @@ -16,6 +16,7 @@ using System.IO; using System.Threading; using Barotrauma.Tutorials; using Barotrauma.Media; +using Barotrauma.Extensions; namespace Barotrauma { @@ -44,7 +45,7 @@ namespace Barotrauma public static ParticleEditorScreen ParticleEditorScreen; public static LevelEditorScreen LevelEditorScreen; public static SpriteEditorScreen SpriteEditorScreen; - public static CharacterEditorScreen CharacterEditorScreen; + public static CharacterEditor.CharacterEditorScreen CharacterEditorScreen; public static Lights.LightManager LightManager; @@ -52,7 +53,7 @@ namespace Barotrauma public static Thread MainThread { get; private set; } - public static HashSet SelectedPackages + public static IEnumerable SelectedPackages { get { return Config?.SelectedContentPackages; } } @@ -171,6 +172,9 @@ namespace Barotrauma GraphicsDeviceManager = new GraphicsDeviceManager(this); + GraphicsDeviceManager.IsFullScreen = false; + GraphicsDeviceManager.ApplyChanges(); + Window.Title = "Barotrauma"; Instance = this; @@ -405,7 +409,7 @@ namespace Barotrauma } } - if (SelectedPackages.Count == 0) + if (SelectedPackages.None()) { DebugConsole.Log("No content packages selected"); } @@ -452,7 +456,9 @@ namespace Barotrauma yield return CoroutineStatus.Running; + Character.LoadAllConfigFiles(); MissionPrefab.Init(); + TraitorMissionPrefab.Init(); MapEntityPrefab.Init(); Tutorials.Tutorial.Init(); MapGenerationParams.Init(); @@ -472,9 +478,9 @@ namespace Barotrauma JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); // Add any missing jobs from the prefab into Config.JobNamePreferences. - foreach (JobPrefab job in JobPrefab.List) + foreach (string job in JobPrefab.List.Keys) { - if (!Config.JobPreferences.Contains(job.Identifier)) { Config.JobPreferences.Add(job.Identifier); } + if (!Config.JobPreferences.Contains(job)) { Config.JobPreferences.Add(job); } } NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); @@ -523,7 +529,7 @@ namespace Barotrauma LevelEditorScreen = new LevelEditorScreen(); SpriteEditorScreen = new SpriteEditorScreen(); - CharacterEditorScreen = new CharacterEditorScreen(); + CharacterEditorScreen = new CharacterEditor.CharacterEditorScreen(); yield return CoroutineStatus.Running; @@ -726,7 +732,7 @@ namespace Barotrauma GameMain.MainMenuScreen.Select(); } UInt64 serverSteamId = SteamManager.SteamIDStringToUInt64(ConnectEndpoint); - Client = new GameClient(SteamManager.GetUsername(), + Client = new GameClient(Config.PlayerName, serverSteamId != 0 ? null : ConnectEndpoint, serverSteamId, string.IsNullOrWhiteSpace(ConnectName) ? ConnectEndpoint : ConnectName); @@ -762,8 +768,11 @@ namespace Barotrauma { GUI.TogglePauseMenu(); } - else if ((Character.Controlled?.SelectedConstruction == null || !Character.Controlled.SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null)) - && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null) + //open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC + else if (Character.Controlled == null || + ((Character.Controlled.SelectedConstruction == null || !Character.Controlled.SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null)) + //TODO: do we need to check Inventory.SelectedSlot? + && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null)) { // Otherwise toggle pausing, unless another window/interface is open. GUI.TogglePauseMenu(); @@ -771,7 +780,7 @@ namespace Barotrauma } GUI.ClearUpdateList(); - paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || Tutorial.ContentRunning) && + paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || Tutorial.ContentRunning || DebugConsole.Paused) && (NetworkMember == null || !NetworkMember.GameStarted); #if !DEBUG @@ -802,6 +811,17 @@ namespace Barotrauma { (GameSession.GameMode as TutorialMode).Update((float)Timing.Step); } + else if (DebugConsole.Paused) + { + if (Screen.Selected.Cam == null) + { + DebugConsole.Paused = false; + } + else + { + Screen.Selected.Cam.MoveCamera((float)Timing.Step); + } + } if (NetworkMember != null) { @@ -908,7 +928,7 @@ namespace Barotrauma UserData = link.Second, OnClicked = (btn, userdata) => { - Process.Start(userdata as string); + ShowOpenUrlInWebBrowserPrompt(userdata as string); return true; } }; @@ -920,7 +940,6 @@ namespace Barotrauma Config.SaveNewPlayerConfig(); } - // ToDo: Move texts/links to localization, when possible. public void ShowBugReporter() { var msgBox = new GUIMessageBox(TextManager.Get("bugreportbutton"), ""); @@ -928,24 +947,27 @@ namespace Barotrauma var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); - List> links = new List>() - { - new Pair(TextManager.Get("bugreportfeedbackform"),"https://barotraumagame.com/feedback"), - new Pair(TextManager.Get("bugreportgithubform"),"https://github.com/Regalis11/Barotrauma/issues/new?template=bug_report.md") - }; - foreach (var link in links) + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), linkHolder.RectTransform), TextManager.Get("bugreportfeedbackform"), style: "MainMenuGUIButton", textAlignment: Alignment.Left) { - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), linkHolder.RectTransform), link.First, style: "MainMenuGUIButton", textAlignment: Alignment.Left) + UserData = "https://steamcommunity.com/app/602960/discussions/1/", + OnClicked = (btn, userdata) => { - UserData = link.Second, - OnClicked = (btn, userdata) => - { - Process.Start(userdata as string); - msgBox.Close(); - return true; - } - }; - } + SteamManager.OverlayCustomURL(userdata as string); + msgBox.Close(); + return true; + } + }; + + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), linkHolder.RectTransform), TextManager.Get("bugreportgithubform"), style: "MainMenuGUIButton", textAlignment: Alignment.Left) + { + UserData = "https://github.com/Regalis11/Barotrauma/issues/new?template=bug_report.md", + OnClicked = (btn, userdata) => + { + ShowOpenUrlInWebBrowserPrompt(userdata as string); + msgBox.Close(); + return true; + } + }; msgBox.InnerFrame.RectTransform.MinSize = new Point(0, msgBox.InnerFrame.Rect.Height + linkHolder.Rect.Height + msgBox.Content.AbsoluteSpacing * 2 + (int)(50 * GUI.Scale)); @@ -968,5 +990,24 @@ namespace Barotrauma if (GameSettings.SaveDebugConsoleLogs) DebugConsole.SaveLogs(); base.OnExiting(sender, args); } + + public void ShowOpenUrlInWebBrowserPrompt(string url) + { + if (string.IsNullOrEmpty(url)) { return; } + if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } + + var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (btn, userdata) => + { + Process.Start(url); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs index 5e8c4aa94..a853eaeda 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs @@ -172,6 +172,11 @@ namespace Barotrauma } var reports = Order.PrefabList.FindAll(o => o.TargetAllCharacters && o.SymbolSprite != null); + 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."); + return; + } reportButtonFrame = new GUILayoutGroup(new RectTransform( new Point((HUDLayoutSettings.CrewArea.Height - (int)((reports.Count - 1) * 5 * GUI.Scale)) / reports.Count, HUDLayoutSettings.CrewArea.Height), guiFrame.RectTransform)) { @@ -322,7 +327,7 @@ namespace Barotrauma /// private GUIComponent CreateCharacterFrame(Character character, GUIComponent parent) { - int correctOrderCount = 0, neutralOrderCount = 0, wrongOrderCount = 0; + int genericOrderCount = 0, correctOrderCount = 0, wrongOrderCount = 0; //sort the orders // 1. generic orders (follow, wait, etc) // 2. orders appropriate for the character's job (captain -> steer, etc) @@ -331,15 +336,16 @@ namespace Barotrauma foreach (Order order in Order.PrefabList) { if (order.TargetAllCharacters || order.SymbolSprite == null) continue; - if (order.AppropriateJobs == null || order.AppropriateJobs.Length == 0) + if (!JobPrefab.List.Values.Any(jp => jp.AppropriateOrders.Contains(order.Identifier)) && + (order.AppropriateJobs == null || !order.AppropriateJobs.Any())) { orders.Insert(0, order); - correctOrderCount++; + genericOrderCount++; } else if (order.HasAppropriateJob(character)) { orders.Add(order); - neutralOrderCount++; + correctOrderCount++; } } foreach (Order order in Order.PrefabList) @@ -481,7 +487,7 @@ namespace Barotrauma var order = orders[i]; if (order.TargetAllCharacters) continue; - RectTransform btnParent = (i >= correctOrderCount + neutralOrderCount) ? + RectTransform btnParent = (i >= genericOrderCount + correctOrderCount) ? wrongOrderList.Content.RectTransform : orderButtonFrame.RectTransform; @@ -516,7 +522,7 @@ namespace Barotrauma if (btn.GetChildByUserData("selected").Visible) { - SetCharacterOrder(character, Order.PrefabList.Find(o => o.AITag == "dismissed"), null, Character.Controlled); + SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, Character.Controlled); } else { @@ -535,7 +541,7 @@ namespace Barotrauma btn.ToolTip = order.Name; //divider between different groups of orders - if (i == correctOrderCount - 1 || i == correctOrderCount + neutralOrderCount - 1) + if (i == genericOrderCount - 1 || i == genericOrderCount + correctOrderCount - 1) { //TODO: divider sprite new GUIFrame(new RectTransform(new Point(8, iconSize), orderButtonFrame.RectTransform), style: "GUIButton"); @@ -999,14 +1005,15 @@ namespace Barotrauma color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f); } - public void HighlightOrderButton(Character character, string orderAiTag, Color color, Vector2? flashRectInflate = null) + public void HighlightOrderButton(Character character, string orderIdentifier, Color color, Vector2? flashRectInflate = null) { - var order = Order.PrefabList.Find(o => o.AITag == orderAiTag); + var order = Order.GetPrefab(orderIdentifier); if (order == null) { - DebugConsole.ThrowError("Could not find an order with the AI tag \"" + orderAiTag + "\".\n" + Environment.StackTrace); + DebugConsole.ThrowError("Could not find an order with the AI tag \"" + orderIdentifier + "\".\n" + Environment.StackTrace); return; } + ToggleCrewAreaOpen = true; var characterElement = characterListBox.Content.FindChild(character); GUIButton orderBtn = characterElement.FindChild(order, recursive: true) as GUIButton; if (orderBtn.Frame.FlashTimer <= 0) @@ -1417,14 +1424,14 @@ namespace Barotrauma // return true; //} - private void ToggleReportButton(string orderAiTag, bool enabled) + private void ToggleReportButton(string orderIdentifier, bool enabled) { - Order order = Order.PrefabList.Find(o => o.AITag == orderAiTag); + Order order = Order.GetPrefab(orderIdentifier); //already reported, disable the button /*if (GameMain.GameSession.CrewManager.ActiveOrders.Any(o => o.First.TargetEntity == Character.Controlled.CurrentHull && - o.First.AITag == orderAiTag)) + o.First.Identifier == orderIdentifier)) { enabled = false; }*/ diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/CampaignMode.cs index 4fed4eec9..d2343012c 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/CampaignMode.cs @@ -9,8 +9,9 @@ namespace Barotrauma { if (Mission == null) return; - new GUIMessageBox(Mission.Name, Mission.Description, new Vector2(0.25f, 0.0f), new Point(400, 200)) + new GUIMessageBox(Mission.Name, Mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: Mission.Prefab.Icon) { + IconColor = Mission.Prefab.IconColor, UserData = "missionstartmessage" }; } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 6cca42753..26b195c90 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -138,6 +138,7 @@ namespace Barotrauma msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex); msg.Write(PurchasedHullRepairs); msg.Write(PurchasedItemRepairs); + msg.Write(PurchasedLostShuttles); msg.Write((UInt16)CargoManager.PurchasedItems.Count); foreach (PurchasedItem pi in CargoManager.PurchasedItems) @@ -164,6 +165,7 @@ namespace Barotrauma int money = msg.ReadInt32(); bool purchasedHullRepairs = msg.ReadBoolean(); bool purchasedItemRepairs = msg.ReadBoolean(); + bool purchasedLostShuttles = msg.ReadBoolean(); UInt16 purchasedItemCount = msg.ReadUInt16(); List purchasedItems = new List(); @@ -178,7 +180,7 @@ namespace Barotrauma CharacterInfo myCharacterInfo = null; if (hasCharacterData) { - myCharacterInfo = CharacterInfo.ClientRead(Character.HumanConfigFile, msg); + myCharacterInfo = CharacterInfo.ClientRead(Character.HumanSpeciesName, msg); } MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; @@ -226,6 +228,7 @@ namespace Barotrauma campaign.Money = money; campaign.PurchasedHullRepairs = purchasedHullRepairs; campaign.PurchasedItemRepairs = purchasedItemRepairs; + campaign.PurchasedLostShuttles = purchasedLostShuttles; campaign.CargoManager.SetPurchasedItems(purchasedItems); if (myCharacterInfo != null) @@ -239,7 +242,6 @@ namespace Barotrauma } campaign.lastUpdateID = updateID; - campaign.SuppressStateSending = false; } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/SinglePlayerCampaign.cs index d098cf3db..dd11e5dfb 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/SinglePlayerCampaign.cs @@ -32,11 +32,11 @@ namespace Barotrauma OnClicked = (btn, userdata) => { TryEndRound(GetLeavingSub()); return true; } }; - foreach (JobPrefab jobPrefab in JobPrefab.List) + foreach (JobPrefab jobPrefab in JobPrefab.List.Values) { for (int i = 0; i < jobPrefab.InitialCount; i++) { - CrewManager.AddCharacterInfo(new CharacterInfo(Character.HumanConfigFile, "", jobPrefab)); + CrewManager.AddCharacterInfo(new CharacterInfo(Character.HumanSpeciesName, "", jobPrefab)); } } } @@ -175,6 +175,11 @@ namespace Barotrauma protected override void WatchmanInteract(Character watchman, Character interactor) { + if (interactor != null) + { + interactor.FocusedCharacter = null; + } + Submarine leavingSub = GetLeavingSub(); if (leavingSub == null) { @@ -182,7 +187,6 @@ namespace Barotrauma return; } - CreateDialog(new List { watchman }, "WatchmanInteract", 1.0f); if (GUIMessageBox.MessageBoxes.Any(mbox => mbox.UserData as string == "watchmanprompt")) @@ -295,7 +299,7 @@ namespace Barotrauma { GameMain.GameSession.LoadPrevious(); GameMain.LobbyScreen.Select(); - GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); + GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData as string == "roundsummary"); return true; } }; @@ -303,7 +307,11 @@ namespace Barotrauma var quitButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("QuitButton")); quitButton.OnClicked += GameMain.LobbyScreen.QuitToMainMenu; - quitButton.OnClicked += (GUIButton button, object obj) => { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); return true; }; + quitButton.OnClicked += (GUIButton button, object obj) => + { + GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData as string == "roundsummary"); + return true; + }; } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs index 0a0677484..a7e03887e 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs @@ -295,9 +295,7 @@ namespace Barotrauma.Tutorials } yield return new WaitForSeconds(1.0f); - var moloch = Character.Create( - "Content/Characters/Moloch/moloch.xml", - steering.Item.WorldPosition + new Vector2(3000.0f, -500.0f), ""); + var moloch = Character.Create("moloch", steering.Item.WorldPosition + new Vector2(3000.0f, -500.0f), ""); moloch.PlaySound(CharacterSound.SoundType.Attack); @@ -663,7 +661,7 @@ namespace Barotrauma.Tutorials //TODO: reimplement //enemy.Health = 50.0f; - enemy.AIController.State = AIController.AIState.Idle; + enemy.AIController.State = AIState.Idle; Vector2 targetPos = Character.Controlled.WorldPosition + new Vector2(0.0f, 3000.0f); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs index fd2bf7ea2..73e18c6f2 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -68,7 +68,7 @@ namespace Barotrauma.Tutorials captainsuniform.Unequip(captain); captain.Inventory.RemoveItem(captainsuniform); - var steerOrder = Order.PrefabList.Find(order => order.AITag == "steer"); + var steerOrder = Order.GetPrefab("steer"); captain_steerIcon = steerOrder.SymbolSprite; captain_steerIconColor = steerOrder.Color; @@ -85,7 +85,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(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "medicaldoctor")); + var medicInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("medicaldoctor")); captain_medic = Character.Create(medicInfo, captain_medicSpawnPos, "medicaldoctor"); captain_medic.GiveJobItems(null); captain_medic.CanSpeak = captain_medic.AIController.Enabled = false; @@ -107,15 +107,15 @@ namespace Barotrauma.Tutorials SetDoorAccess(tutorial_lockedDoor_1, null, false); SetDoorAccess(tutorial_lockedDoor_2, null, false); - var mechanicInfo = new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "mechanic")); + var mechanicInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("mechanic")); captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "mechanic"); captain_mechanic.GiveJobItems(); - var securityInfo = new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "securityofficer")); + var securityInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("securityofficer")); captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "securityofficer"); captain_security.GiveJobItems(); - var engineerInfo = new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "engineer")); + var engineerInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("engineer")); captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "engineer"); captain_engineer.GiveJobItems(); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 37a99239c..98fce58fb 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -48,7 +48,7 @@ namespace Barotrauma.Tutorials { base.Start(); - var firstAidOrder = Order.PrefabList.Find(order => order.AITag == "requestfirstaid"); + var firstAidOrder = Order.GetPrefab("requestfirstaid"); doctor_firstAidIcon = firstAidOrder.SymbolSprite; doctor_firstAidIconColor = firstAidOrder.Color; @@ -63,30 +63,30 @@ 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(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "assistant")); + var assistantInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("assistant")); patient1 = Character.Create(assistantInfo, patientHull1.WorldPosition, "1"); patient1.GiveJobItems(null); patient1.CanSpeak = false; patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 45.0f) }, stun: 0, playSound: false); patient1.AIController.Enabled = false; - - assistantInfo = new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "assistant")); + + assistantInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("assistant")); patient2 = Character.Create(assistantInfo, patientHull2.WorldPosition, "2"); patient2.GiveJobItems(null); patient2.CanSpeak = false; patient2.AIController.Enabled = false; - var mechanicInfo = new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "engineer")); + var mechanicInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("engineer")); var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "3"); subPatient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient1); - var securityInfo = new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "securityofficer")); + var securityInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("securityofficer")); var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "3"); subPatient2.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.InternalDamage, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient2); - var engineerInfo = new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "engineer")); + var engineerInfo = new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("engineer")); var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "3"); subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient3); @@ -240,7 +240,7 @@ namespace Barotrauma.Tutorials // treat patient -------------------------------------------------------------------------------------------- //patient 1 requests first aid - var newOrder = new Order(Order.PrefabList.Find(o => o.AITag == "requestfirstaid"), patient1.CurrentHull, null, orderGiver: patient1); + var newOrder = new Order(Order.GetPrefab("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); @@ -263,7 +263,7 @@ namespace Barotrauma.Tutorials doctor.RemoveActiveObjectiveEntity(patient1); TriggerTutorialSegment(3); // Get the patient to medbay - while (patient1.CurrentOrder == null || patient1.CurrentOrder.AITag != "follow") + while (patient1.CurrentOrder == null || patient1.CurrentOrder.Identifier != "follow") { GameMain.GameSession.CrewManager.HighlightOrderButton(patient1, "follow", highlightColor, new Vector2(5, 5)); yield return null; @@ -329,7 +329,7 @@ namespace Barotrauma.Tutorials //patient calls for help //patient2.CanSpeak = true; yield return new WaitForSeconds(2.0f, false); - newOrder = new Order(Order.PrefabList.Find(o => o.AITag == "requestfirstaid"), patient2.CurrentHull, null, orderGiver: patient2); + newOrder = new Order(Order.GetPrefab("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); @@ -396,7 +396,7 @@ namespace Barotrauma.Tutorials if (!patientCalledHelp[i] && Timing.TotalTime > subEnterTime + 60 * (i + 1)) { doctor.AddActiveObjectiveEntity(subPatients[i], doctor_firstAidIcon, doctor_firstAidIconColor); - newOrder = new Order(Order.PrefabList.Find(o => o.AITag == "requestfirstaid"), subPatients[i].CurrentHull, null, orderGiver: subPatients[i]); + newOrder = new Order(Order.GetPrefab("requestfirstaid"), subPatients[i].CurrentHull, null, orderGiver: subPatients[i]); string message = newOrder.GetChatMessage("", subPatients[i].CurrentHull?.DisplayName, givingOrderToSelf: false); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(subPatients[i].Name, message, ChatMessageType.Order, null); patientCalledHelp[i] = true; diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs index 1824f4b0f..87188c524 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -91,11 +91,11 @@ namespace Barotrauma.Tutorials toolbox.Unequip(engineer); engineer.Inventory.RemoveItem(toolbox); - var repairOrder = Order.PrefabList.Find(order => order.AITag == "repairsystems"); + var repairOrder = Order.GetPrefab("repairsystems"); engineer_repairIcon = repairOrder.SymbolSprite; engineer_repairIconColor = repairOrder.Color; - var reactorOrder = Order.PrefabList.Find(order => order.AITag == "operatereactor"); + var reactorOrder = Order.GetPrefab("operatereactor"); engineer_reactorIcon = reactorOrder.SymbolSprite; engineer_reactorIconColor = reactorOrder.Color; diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 7f2144286..db5d5f16f 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -95,7 +95,7 @@ namespace Barotrauma.Tutorials crowbar.Unequip(mechanic); mechanic.Inventory.RemoveItem(crowbar); - var repairOrder = Order.PrefabList.Find(order => order.AITag == "repairsystems"); + var repairOrder = Order.GetPrefab("repairsystems"); mechanic_repairIcon = repairOrder.SymbolSprite; mechanic_repairIconColor = repairOrder.Color; mechanic_weldIcon = new Sprite("Content/UI/IconAtlas.png", new Rectangle(1, 256, 127, 127), new Vector2(0.5f, 0.5f)); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 5d8e66fec..857c6bc15 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -74,18 +74,12 @@ namespace Barotrauma.Tutorials // Variables private string radioSpeakerName; private Character officer; - private string crawlerCharacterFile; - private string hammerheadCharacterFile; - private string mudraptorCharacterFile; private float superCapacitorRechargeRate = 10; private Sprite officer_gunIcon; private Color officer_gunIconColor; public OfficerTutorial(XElement element) : base(element) { - crawlerCharacterFile = Character.GetConfigFile("crawler"); - hammerheadCharacterFile = Character.GetConfigFile("hammerhead"); - mudraptorCharacterFile = Character.GetConfigFile("mudraptor"); } public override void Start() @@ -111,7 +105,7 @@ namespace Barotrauma.Tutorials bodyarmor.Unequip(officer); officer.Inventory.RemoveItem(bodyarmor); - var gunOrder = Order.PrefabList.Find(order => order.AITag == "operateweapons"); + var gunOrder = Order.GetPrefab("operateweapons"); officer_gunIcon = gunOrder.SymbolSprite; officer_gunIconColor = gunOrder.Color; @@ -267,7 +261,7 @@ namespace Barotrauma.Tutorials // Room 3 do { yield return null; } while (!officer_crawlerSensor.MotionDetected); TriggerTutorialSegment(2); - officer_crawler = SpawnMonster(crawlerCharacterFile, officer_crawlerSpawnPos); + officer_crawler = SpawnMonster("crawler", officer_crawlerSpawnPos); do { yield return null; } while (!officer_crawler.IsDead); RemoveCompletedObjective(segments[2]); Heal(officer); @@ -298,7 +292,7 @@ namespace Barotrauma.Tutorials RemoveCompletedObjective(segments[3]); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(4, GameMain.Config.KeyBind(InputType.Select), GameMain.Config.KeyBind(InputType.Shoot), GameMain.Config.KeyBind(InputType.Deselect)); // Kill hammerhead - officer_hammerhead = SpawnMonster(hammerheadCharacterFile, officer_hammerheadSpawnPos); + officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); officer_hammerhead.AIController.SelectTarget(officer.AiTarget); SetHighlight(officer_coilgunPeriscope, true); float originalDistance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerheadSpawnPos); @@ -314,8 +308,8 @@ namespace Barotrauma.Tutorials { // Ensure that the Hammerhead targets the player officer_hammerhead.AIController.SelectTarget(officer.AiTarget); - var ai = officer_hammerhead.AIController as EnemyAIController; - ai.sight = 2.0f; + /*var ai = officer_hammerhead.AIController as EnemyAIController; + ai.sight = 2.0f;*/ } yield return null; } @@ -381,7 +375,7 @@ namespace Barotrauma.Tutorials // Room 6 do { yield return null; } while (!officer_mudraptorObjectiveSensor.MotionDetected); TriggerTutorialSegment(6); - officer_mudraptor = SpawnMonster(mudraptorCharacterFile, officer_mudraptorSpawnPos); + officer_mudraptor = SpawnMonster("mudraptor", officer_mudraptorSpawnPos); do { yield return null; } while (!officer_mudraptor.IsDead); Heal(officer); RemoveCompletedObjective(segments[6]); @@ -447,9 +441,9 @@ namespace Barotrauma.Tutorials return officer?.SelectedConstruction == item; } - private Character SpawnMonster(string characterFile, Vector2 pos) + private Character SpawnMonster(string speciesName, Vector2 pos) { - var character = Character.Create(characterFile, pos, ToolBox.RandomSeed(8)); + var character = Character.Create(speciesName, pos, ToolBox.RandomSeed(8)); var ai = character.AIController as EnemyAIController; ai.TargetOutposts = true; character.CharacterHealth.SetVitality(character.Health / 2); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 07f73f766..60b1e3c3b 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -102,7 +102,7 @@ namespace Barotrauma.Tutorials Submarine.MainSub.GodMode = true; CharacterInfo charInfo = configElement.Element("Character") == null ? - new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "engineer")) : + new CharacterInfo(Character.HumanSpeciesName, "", JobPrefab.Get("engineer")) : new CharacterInfo(configElement.Element("Character")); WayPoint wayPoint = GetSpawnPoint(charInfo); @@ -176,9 +176,9 @@ namespace Barotrauma.Tutorials return WayPoint.GetRandom(spawnPointType, charInfo.Job, spawnSub); } - protected bool HasOrder(Character character, string aiTag, string option = null) + protected bool HasOrder(Character character, string identifier, string option = null) { - if (character.CurrentOrder?.AITag == aiTag) + if (character.CurrentOrder?.Identifier == identifier) { if (option == null) { diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs index 518288032..40a5ca72f 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs @@ -32,7 +32,10 @@ namespace Barotrauma SoundPlayer.OverrideMusicDuration = 18.0f; } - GUIFrame frame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); + GUIFrame frame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + { + UserData = "roundsummary" + }; int width = 760, height = 500; GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.5f), frame.RectTransform, Anchor.Center, minSize: new Point(width, height))); diff --git a/Barotrauma/BarotraumaClient/Source/GameSettings.cs b/Barotrauma/BarotraumaClient/Source/GameSettings.cs index a7ecbd3c3..b4b7e2a49 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSettings.cs @@ -22,6 +22,8 @@ namespace Barotrauma private readonly Point MinSupportedResolution = new Point(1024, 540); + private bool contentPackageSelectionDirty; + private GUIFrame settingsFrame; private GUIButton applyButton; @@ -122,18 +124,28 @@ namespace Barotrauma } if (!contentPackage.IsCompatible()) { - tickBox.TextColor = Color.Red; tickBox.Enabled = false; - tickBox.ToolTip = TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", + tickBox.TextColor = Color.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.CorePackage && !contentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) { - tickBox.TextColor = Color.Red; tickBox.Enabled = false; - tickBox.ToolTip = TextManager.GetWithVariables("ContentPackageMissingCoreFiles", new string[2] { "[packagename]", "[missingfiletypes]" }, + tickBox.TextColor = Color.Red * 0.6f; + tickBox.ToolTip = tickBox.TextBlock.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.Invalid) + { + tickBox.Enabled = false; + tickBox.TextColor = Color.Red * 0.6f; + tickBox.ToolTip = tickBox.TextBlock.ToolTip = + TextManager.GetWithVariable("InvalidContentPackage", "[packagename]", contentPackage.Name) + + "\n" + string.Join("\n", contentPackage.ErrorMessages); + } } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.045f), generalLayoutGroup.RectTransform), TextManager.Get("Language")); @@ -147,12 +159,28 @@ namespace Barotrauma languageDD.OnSelected = (guiComponent, obj) => { string newLanguage = obj as string; - if (newLanguage == Language) return true; - + if (newLanguage == Language) { return true; } + + string prevLanguage = Language; Language = newLanguage; UnsavedSettings = true; - var msgBox = new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredLanguage")); + var msgBox = new GUIMessageBox( + TextManager.Get("RestartRequiredLabel"), + TextManager.Get("RestartRequiredLanguage"), + buttons: new string[] { TextManager.Get("Cancel"), TextManager.Get("OK") }); + msgBox.Buttons[0].OnClicked += (btn, userdata) => + { + Language = prevLanguage; + languageDD.SelectItem(Language); + msgBox.Close(); + return true; + }; msgBox.Buttons[1].OnClicked += (btn, userdata) => + { + ApplySettings(); + GameMain.Instance.Exit(); + return true; + }; return true; }; @@ -586,7 +614,7 @@ namespace Barotrauma BarScroll = (float)Math.Sqrt(MathUtils.InverseLerp(0.2f, 5.0f, MicrophoneVolume)), OnMoved = (scrollBar, scroll) => { - MicrophoneVolume = MathHelper.Lerp(0.2f, 5.0f, scroll * scroll); + MicrophoneVolume = MathHelper.Lerp(0.2f, 10.0f, scroll * scroll); MicrophoneVolume = (float)Math.Round(MicrophoneVolume, 1); ChangeSliderText(scrollBar, MicrophoneVolume); scrollBar.Step = 0.05f; @@ -630,7 +658,7 @@ namespace Barotrauma noiseGateSlider.Frame.Visible = false; noiseGateSlider.Step = 0.01f; noiseGateSlider.Range = new Vector2(-100.0f, 0.0f); - noiseGateSlider.BarScroll = MathUtils.InverseLerp(-1.0f, 0.0f, NoiseGateThreshold); + noiseGateSlider.BarScroll = MathUtils.InverseLerp(-100.0f, 0.0f, NoiseGateThreshold); noiseGateSlider.BarScroll *= noiseGateSlider.BarScroll; noiseGateSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { @@ -976,14 +1004,15 @@ namespace Barotrauma private bool SelectContentPackage(GUITickBox tickBox) { + contentPackageSelectionDirty = true; var contentPackage = tickBox.UserData as ContentPackage; if (contentPackage.CorePackage) { if (tickBox.Selected) { //make sure no other core packages are selected - SelectedContentPackages.RemoveWhere(cp => cp.CorePackage && cp != contentPackage); - SelectedContentPackages.Add(contentPackage); + SelectedContentPackages.RemoveAll(cp => cp.CorePackage && cp != contentPackage); + SelectContentPackage(contentPackage); foreach (GUITickBox otherTickBox in tickBox.Parent.Children) { ContentPackage otherContentPackage = otherTickBox.UserData as ContentPackage; @@ -1003,11 +1032,11 @@ namespace Barotrauma { if (tickBox.Selected) { - SelectedContentPackages.Add(contentPackage); + SelectContentPackage(contentPackage); } else { - SelectedContentPackages.Remove(contentPackage); + DeselectContentPackage(contentPackage); } } if (contentPackage.GetFilesOfType(ContentType.Submarine).Any()) { Submarine.RefreshSavedSubs(); } @@ -1128,7 +1157,10 @@ namespace Barotrauma { ApplySettings(); if (Screen.Selected != GameMain.MainMenuScreen) GUI.SettingsMenuOpen = false; - + if (contentPackageSelectionDirty) + { + new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredGeneric")); + } return true; } } diff --git a/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs index 800766b64..4f2a5eb2d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs @@ -113,7 +113,13 @@ namespace Barotrauma if (item == null) return null; var container = item.GetComponent(); - if (container == null || !container.KeepOpenWhenEquipped || !character.HasEquippedItem(container.Item)) return null; + if (container == null || + !character.CanAccessInventory(container.Inventory) || + !container.KeepOpenWhenEquipped || + !character.HasEquippedItem(container.Item)) + { + return null; + } return container.Inventory; } @@ -133,7 +139,7 @@ namespace Barotrauma public override void CreateSlots() { - if (slots == null) slots = new InventorySlot[capacity]; + if (slots == null) { slots = new InventorySlot[capacity]; } for (int i = 0; i < capacity; i++) { @@ -179,9 +185,11 @@ namespace Barotrauma highlightedSubInventorySlots.RemoveWhere(s => s.Inventory.OpenState <= 0.0f); foreach (var subSlot in highlightedSubInventorySlots) { - subSlot.Slot = slots[subSlot.SlotIndex]; + if (subSlot.ParentInventory == this && subSlot.SlotIndex > 0 && subSlot.SlotIndex < slots.Length) + { + subSlot.Slot = slots[subSlot.SlotIndex]; + } } - //highlightedSubInventorySlots.Clear(); screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); CalculateBackgroundFrame(); @@ -475,7 +483,9 @@ namespace Barotrauma } List hideSubInventories = new List(); - highlightedSubInventorySlots.RemoveWhere(s => s.SlotIndex < 0 || s.SlotIndex >= Items.Length || Items[s.SlotIndex] == null); + highlightedSubInventorySlots.RemoveWhere(s => + s.ParentInventory == this && + ((s.SlotIndex < 0 || s.SlotIndex >= Items.Length || Items[s.SlotIndex] == null) || (Character.Controlled != null && !Character.Controlled.CanAccessInventory(s.Inventory)))); foreach (var highlightedSubInventorySlot in highlightedSubInventorySlots) { if (highlightedSubInventorySlot.ParentInventory == this) @@ -522,6 +532,9 @@ namespace Barotrauma if (character.SelectedCharacter == null) // Permanently open subinventories only available when the default UI layout is in use -> not when grabbing characters { + //remove the highlighted slots of other characters' inventories when not grabbing anyone + highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory != this && s.ParentInventory?.Owner is Character); + for (int i = 0; i < capacity; i++) { var item = Items[i]; @@ -531,7 +544,10 @@ namespace Barotrauma if (character.HasEquippedItem(item)) // Keep a subinventory display open permanently when the container is equipped { var itemContainer = item.GetComponent(); - if (itemContainer != null && itemContainer.KeepOpenWhenEquipped && !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) + if (itemContainer != null && + itemContainer.KeepOpenWhenEquipped && + character.CanAccessInventory(itemContainer.Inventory) && + !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) { ShowSubInventory(new SlotReference(this, slots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true); } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs index 8dd924ac8..0fa67f1c0 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components //openState when the vertices of the convex hull were last calculated private float lastConvexHullState; - [Serialize("1,1", false)] + [Serialize("1,1", false, description: "The scale of the shadow-casting area of the door (relative to the actual size of the door).")] public Vector2 ShadowScale { get; @@ -102,7 +102,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { Color color = item.SpriteColor; if (brokenSprite == null) @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components if (stuck > 0.0f && weldedSprite != null) { Vector2 weldSpritePos = new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height / 2.0f); - if (item.Submarine != null) weldSpritePos += item.Submarine.Position; + if (item.Submarine != null) weldSpritePos += item.Submarine.DrawPosition; weldSpritePos.Y = -weldSpritePos.Y; weldedSprite.Draw(spriteBatch, diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs index d1ef6d350..9ae28d15c 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs @@ -400,7 +400,7 @@ namespace Barotrauma.Items.Components string style = subElement.Attribute("style") == null ? null : subElement.GetAttributeString("style", ""); - GuiFrame = new GUIFrame(RectTransform.Load(subElement, GUI.Canvas), style, color); + GuiFrame = new GUIFrame(RectTransform.Load(subElement, GUI.Canvas, Anchor.Center), style, color); DefaultLayout = GUILayoutSettings.Load(subElement); break; case "alternativelayout": diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs index afcfb1212..5fafa9498 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs @@ -33,49 +33,49 @@ namespace Barotrauma.Items.Components } #if DEBUG - [Serialize("0.0,0.0", false), Editable] -#else - [Serialize("0.0,0.0", false)] + [Editable] #endif + [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).")] public Vector2 ItemPos { get; set; } #if DEBUG - [Serialize("0.0,0.0", false), Editable] -#else - [Serialize("0.0,0.0", false)] + [Editable] #endif + [Serialize("0.0,0.0", false, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] public Vector2 ItemInterval { get; set; } - [Serialize(100, false)] + [Serialize(100, false, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } /// /// Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used. /// - [Serialize(-1.0f, false)] + [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.")] public float ContainedSpriteDepth { get; set; } private float itemRotation; - [Serialize(0.0f, false)] + [Serialize(0.0f, false, 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(null, false)] + [Serialize(null, false, description: "An optional text displayed above the item's inventory.")] public string UILabel { get; set; } - [Serialize(false, false)] + [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." + + " 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 { get; set; } - [Serialize(false, false)] + [Serialize(false, false, 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)] + [Serialize(false, false, description: "Can the inventory of this item be moved around on the screen by the player.")] public bool MovableFrame { get; set; } public Vector2 DrawSize @@ -132,13 +132,13 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing = false) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (hideItems || (item.body != null && !item.body.Enabled)) { return; } - DrawContainedItems(spriteBatch); + DrawContainedItems(spriteBatch, itemDepth); } - public void DrawContainedItems(SpriteBatch spriteBatch) + public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth) { Vector2 transformedItemPos = ItemPos * item.Scale; Vector2 transformedItemInterval = ItemInterval * item.Scale; @@ -199,20 +199,28 @@ namespace Barotrauma.Items.Components { containedItem.body.SetTransformIgnoreContacts(containedItem.body.SimPosition, currentRotation); } + + Vector2 origin = containedItem.Sprite.Origin; + if (item.FlippedX) { origin.X = containedItem.Sprite.SourceRect.Width - origin.X; } + if (item.FlippedY) { origin.Y = containedItem.Sprite.SourceRect.Height - origin.Y; } + + float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth; + containedSpriteDepth = itemDepth + (containedSpriteDepth - item.SpriteDepth) / 10000.0f; containedItem.Sprite.Draw( spriteBatch, new Vector2(currentItemPos.X, -currentItemPos.Y), containedItem.GetSpriteColor(), - -currentRotation, + origin, + - currentRotation, containedItem.Scale, spriteEffects, - depth: ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth); + depth: containedSpriteDepth); foreach (ItemContainer ic in containedItem.GetComponents()) { if (ic.hideItems) continue; - ic.DrawContainedItems(spriteBatch); + ic.DrawContainedItems(spriteBatch, containedSpriteDepth); } i++; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemLabel.cs index c837950c7..d1d8c589d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemLabel.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components private float[] charWidths; - [Serialize("0,0,0,0", true)] + [Serialize("0,0,0,0", true, description: "The amount of padding around the text in pixels (left,top,right,bottom). ")] public Vector4 Padding { get { return TextBlock.Padding; } @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components } private string text; - [Serialize("", true, translationTextTag: "Label."), Editable(100)] + [Serialize("", true, translationTextTag: "Label.", description: "The text displayed in the label."), Editable(100)] public string Text { get { return text; } @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components private set; } - [Editable, Serialize("0.0,0.0,0.0,1.0", true)] + [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label (R,G,B,A).")] public Color TextColor { get { return textColor; } @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, 10.0f), Serialize(1.0f, true)] + [Editable(0.0f, 10.0f), Serialize(1.0f, true, description: "The scale of the text displayed on the label.")] public float TextScale { get { return textBlock == null ? 1.0f : textBlock.TextScale; } @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components } private bool scrollable; - [Serialize(false, true)] + [Serialize(false, true, 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; } @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(20.0f, true)] + [Serialize(20.0f, true, description: "How fast the text scrolls across the item (only valid if Scrollable is set to true).")] public float ScrollSpeed { get; @@ -202,7 +202,7 @@ namespace Barotrauma.Items.Components TextBlock.Text = sb.ToString(); } - public void Draw(SpriteBatch spriteBatch, bool editing = false) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { var drawPos = new Vector2( item.DrawPosition.X - item.Rect.Width / 2.0f, diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs index 8c25475ee..891eb7036 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components get { return light; } } - public void Draw(SpriteBatch spriteBatch, bool editing = false) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f) { @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components public override void FlipX(bool relativeToSub) { - if (light?.LightSprite != null) + if (light?.LightSprite != null && item.Prefab.CanSpriteFlipX) { light.LightSpriteEffect = light.LightSpriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs index 81c00d2ab..bf9d0fcc3 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs @@ -113,7 +113,7 @@ namespace Barotrauma.Items.Components if (spriteIndex >= propellerSprite.FrameCount) spriteIndex = 0.0f; } - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (propellerSprite != null) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs index bd7bb363a..b97f83018 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs @@ -271,11 +271,14 @@ namespace Barotrauma.Items.Components Rectangle slotRect = outputContainer.Inventory.slots[0].Rect; - GUI.DrawRectangle(spriteBatch, - new Rectangle( - slotRect.X, slotRect.Y + (int)(slotRect.Height * (1.0f - progressState)), - slotRect.Width, (int)(slotRect.Height * progressState)), - Color.Green * 0.5f, isFilled: true); + if (fabricatedItem != null) + { + GUI.DrawRectangle(spriteBatch, + new Rectangle( + slotRect.X, slotRect.Y + (int)(slotRect.Height * (1.0f - progressState)), + slotRect.Width, (int)(slotRect.Height * progressState)), + Color.Green * 0.5f, isFilled: true); + } itemIcon.Draw( spriteBatch, diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs index a6d4c56d4..b9f7fc697 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs @@ -79,6 +79,11 @@ namespace Barotrauma.Items.Components displayedSubs.AddRange(item.Submarine.DockedTo); } + public override void FlipX(bool relativeToSub) + { + CreateHUD(); + } + public override void UpdateHUD(Character character, float deltaTime, Camera cam) { //recreate HUD if the subs we should display have changed diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs index 146c3cc52..b64362833 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs @@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components var btnText = warningBtn.GetChild(); btnText.Font = GUI.Font; - btnText.Wrap = true; + btnText.Wrap = false; btnText.SetTextPos(); warningButtons.Add(warningTexts[i], warningBtn); } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs index 295bb17cd..330ef05df 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs @@ -27,12 +27,16 @@ namespace Barotrauma.Items.Components private GUITickBox directionalTickBox; private GUIScrollBar directionalSlider; + private Vector2? pingDragDirection = null; private GUILayoutGroup activeControlsContainer; private GUIFrame controlContainer; private GUICustomComponent sonarView; + private Sprite directionalPingBackground; + private Sprite[] directionalPingButton; + private float displayBorderSize; private List sonarBlips; @@ -149,14 +153,10 @@ namespace Barotrauma.Items.Components { showDirectionalIndicatorTimer = 1.0f; float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, scroll); - pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle)); - if (GameMain.Client != null) - { - unsentChanges = true; - correctionTimer = CorrectionDelay; - } + SetPingDirection(new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle))); return true; - } + }, + Range = new Vector2(0,MathHelper.TwoPi) }; signalWarningText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), paddedControlContainer.RectTransform), "", Color.Orange, textAlignment: Alignment.Center); @@ -178,6 +178,14 @@ namespace Barotrauma.Items.Components case "directionalpingcircle": directionalPingCircle = new Sprite(subElement); break; + case "directionalpingbackground": + directionalPingBackground = new Sprite(subElement); + break; + case "directionalpingbutton": + if (directionalPingButton == null) { directionalPingButton = new Sprite[3]; } + int index = subElement.GetAttributeInt("index", 0); + directionalPingButton[index] = new Sprite(subElement); + break; case "screenoverlay": screenOverlay = new Sprite(subElement); break; @@ -206,6 +214,16 @@ namespace Barotrauma.Items.Components controlContainer.RectTransform.AbsoluteOffset = new Point((int)(viewSize * 0.9f), 0); } + private void SetPingDirection(Vector2 direction) + { + pingDirection = direction; + if (GameMain.Client != null) + { + unsentChanges = true; + correctionTimer = CorrectionDelay; + } + } + public override void OnItemLoaded() { zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); @@ -352,6 +370,43 @@ namespace Barotrauma.Items.Components prevDockingDist = float.MaxValue; } + if (steering != null && directionalPingButton != null) + { + steering.SteerRadius = useDirectionalPing && pingDragDirection != null ? + -1.0f : + PlayerInput.LeftButtonDown() || !PlayerInput.LeftButtonHeld() ? + (float?)((sonarView.Rect.Width / 2) - (directionalPingButton[0].size.X * sonarView.Rect.Width / screenBackground.size.X)) : + null; + } + + if (useDirectionalPing && PlayerInput.LeftButtonHeld()) + { + if ((MouseInDirectionalPingRing(sonarView.Rect, false) && PlayerInput.LeftButtonDown()) || pingDragDirection != null) + { + Vector2 newDragDir = Vector2.Normalize(PlayerInput.MousePosition - sonarView.Rect.Center.ToVector2()); + if (pingDragDirection == null && !MouseInDirectionalPingRing(sonarView.Rect, true)) + { + directionalSlider.BarScrollValue = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(newDragDir)); + directionalSlider.OnMoved(directionalSlider, directionalSlider.BarScroll); + } + else if (pingDragDirection != null) + { + float newAngle = MathUtils.VectorToAngle(newDragDir); + float oldAngle = MathUtils.VectorToAngle(pingDragDirection.Value); + float pingAngle = MathUtils.VectorToAngle(pingDirection); + pingAngle = MathUtils.WrapAngleTwoPi(pingAngle + MathUtils.GetShortestAngle(oldAngle, newAngle)); + directionalSlider.BarScrollValue = pingAngle; + directionalSlider.OnMoved(directionalSlider, directionalSlider.BarScroll); + } + + pingDragDirection = newDragDir; + } + } + else + { + pingDragDirection = null; + } + for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) { var activePing = activePings[pingIndex]; @@ -391,6 +446,28 @@ namespace Barotrauma.Items.Components prevPassivePingRadius = passivePingRadius; } + private bool MouseInDirectionalPingRing(Rectangle rect, bool onButton) + { + if (!useDirectionalPing || directionalPingButton == null) { return false; } + + float endRadius = rect.Width / 2.0f; + float startRadius = endRadius - directionalPingButton[0].size.X * rect.Width / screenBackground.size.X; + + Vector2 center = rect.Center.ToVector2(); + + float dist = Vector2.DistanceSquared(PlayerInput.MousePosition,center); + + bool retVal = (dist >= startRadius*startRadius) && (dist < endRadius*endRadius); + if (onButton) + { + float pingAngle = MathUtils.VectorToAngle(pingDirection); + float mouseAngle = MathUtils.VectorToAngle(Vector2.Normalize(PlayerInput.MousePosition - center)); + retVal &= Math.Abs(MathUtils.GetShortestAngle(mouseAngle, pingAngle)) < MathHelper.ToRadians(DirectionalPingSector * 0.5f); + } + + return retVal; + } + private void DrawSonar(SpriteBatch spriteBatch, Rectangle rect) { displayBorderSize = 0.2f; @@ -403,6 +480,24 @@ namespace Barotrauma.Items.Components screenBackground.Draw(spriteBatch, center, 0.0f, rect.Width / screenBackground.size.X); } + if (useDirectionalPing) + { + directionalPingBackground?.Draw(spriteBatch, center, 0.0f, rect.Width / directionalPingBackground.size.X); + if (directionalPingButton != null) + { + int buttonSprIndex = 0; + if (pingDragDirection != null) + { + buttonSprIndex = 2; + } + else if (MouseInDirectionalPingRing(rect, true)) + { + buttonSprIndex = 1; + } + directionalPingButton[buttonSprIndex]?.Draw(spriteBatch, center, MathUtils.VectorToAngle(pingDirection), rect.Width / directionalPingBackground.size.X); + } + } + if (currentMode == Mode.Active && currentPingIndex != -1) { var activePing = activePings[currentPingIndex]; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs index f783f5b3c..595592103 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs @@ -47,6 +47,10 @@ namespace Barotrauma.Items.Components private Sprite maintainPosIndicator, maintainPosOriginIndicator; private Sprite steeringIndicator; + private List connectedPorts = new List(); + private float checkConnectedPortsTimer; + private const float CheckConnectedPortsInterval = 1.0f; + private Vector2 keyboardInput = Vector2.Zero; private float inputCumulation; @@ -86,6 +90,19 @@ namespace Barotrauma.Items.Components set; } = true; + private float steerRadius; + public float? SteerRadius + { + get + { + return steerRadius; + } + set + { + steerRadius = value ?? (steerArea.Rect.Width / 2); + } + } + public List DockingSources = new List(); public DockingPort ActiveDockingSource, DockingTarget; @@ -317,7 +334,7 @@ namespace Barotrauma.Items.Components { if (GameMain.Client == null) { - item.SendSignal(0, "1", "toggle_docking", sender: Character.Controlled); + item.SendSignal(0, "1", "toggle_docking", sender: null); } else { @@ -384,6 +401,8 @@ namespace Barotrauma.Items.Components statusContainer.RectTransform.AbsoluteOffset = new Point((int)(viewSize * 0.9f), 0); steerArea.RectTransform.NonScaledSize = new Point(viewSize); dockingContainer.RectTransform.AbsoluteOffset = new Point((int)(viewSize * 0.9f), 0); + + steerRadius = steerArea.Rect.Width / 2; } private void FindConnectedDockingPort() @@ -661,7 +680,7 @@ namespace Barotrauma.Items.Components pressureWarningText.Visible = item.Submarine != null && item.Submarine.AtDamageDepth && Timing.TotalTime % 1.0f < 0.5f; - if (Vector2.Distance(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerArea.Rect.Width / 2) + if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { if (PlayerInput.LeftButtonHeld()) { @@ -735,10 +754,21 @@ namespace Barotrauma.Items.Components } if (!UseAutoDocking) { return; } + + if (checkConnectedPortsTimer <= 0.0f) + { + Connection dockingConnection = item.Connections?.FirstOrDefault(c => c.Name == "toggle_docking"); + if (dockingConnection != null) + { + connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection); + } + checkConnectedPortsTimer = CheckConnectedPortsInterval; + } float closestDist = DockingAssistThreshold * DockingAssistThreshold; DockingModeEnabled = false; - foreach (DockingPort sourcePort in DockingPort.List) + + foreach (DockingPort sourcePort in connectedPorts) { if (sourcePort.Docked || sourcePort.Item.Submarine == null) { continue; } if (sourcePort.Item.Submarine != controlledSub) { continue; } @@ -826,6 +856,7 @@ namespace Barotrauma.Items.Components int msgStartPos = msg.BitPosition; bool autoPilot = msg.ReadBoolean(); + bool dockingButtonClicked = msg.ReadBoolean(); Vector2 newSteeringInput = steeringInput; Vector2 newTargetVelocity = targetVelocity; float newSteeringAdjustSpeed = steeringAdjustSpeed; @@ -833,6 +864,11 @@ namespace Barotrauma.Items.Components Vector2? newPosToMaintain = null; bool headingToStart = false; + if (dockingButtonClicked) + { + item.SendSignal(0, "1", "toggle_docking", sender: null); + } + if (autoPilot) { maintainPos = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs index 143bbe182..3c2f9a3e2 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs @@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components chargeIndicator.Color = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); } - public void Draw(SpriteBatch spriteBatch, bool editing = false) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (indicatorSize.X <= 1.0f || indicatorSize.Y <= 1.0f) return; @@ -143,7 +143,10 @@ namespace Barotrauma.Items.Components float rechargeRate = msg.ReadRangedInteger(0, 10) / 10.0f; RechargeSpeed = rechargeRate * MaxRechargeSpeed; #if CLIENT - rechargeSpeedSlider.BarScroll = rechargeRate; + if (rechargeSpeedSlider != null) + { + rechargeSpeedSlider.BarScroll = rechargeRate; + } #endif Charge = msg.ReadRangedSingle(0.0f, 1.0f, 8) * capacity; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs index 842ec0e92..19d2d4caf 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs @@ -16,21 +16,17 @@ namespace Barotrauma.Items.Components : IDrawableComponent #endif { - public ParticleEmitter ParticleEmitter - { - get; - private set; - } #if DEBUG public Vector2 DrawSize { get { return GameMain.DebugDraw ? Vector2.One * Range : Vector2.Zero; } - } + } #endif - private List ParticleEmitterHitStructure = new List(); - private List ParticleEmitterHitCharacter = new List(); - private List> ParticleEmitterHitItem = new List>(); + private List particleEmitters = new List(); + private List particleEmitterHitStructure = new List(); + private List particleEmitterHitCharacter = new List(); + private List> particleEmitterHitItem = new List>(); private float prevProgressBarState; @@ -41,7 +37,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "particleemitter": - ParticleEmitter = new ParticleEmitter(subElement); + particleEmitters.Add(new ParticleEmitter(subElement)); break; case "particleemitterhititem": string[] identifiers = subElement.GetAttributeStringArray("identifiers", new string[0]); @@ -49,16 +45,16 @@ namespace Barotrauma.Items.Components string[] excludedIdentifiers = subElement.GetAttributeStringArray("excludedidentifiers", new string[0]); if (excludedIdentifiers.Length == 0) excludedIdentifiers = subElement.GetAttributeStringArray("excludedidentifier", new string[0]); - ParticleEmitterHitItem.Add( + particleEmitterHitItem.Add( new Pair( new RelatedItem(identifiers, excludedIdentifiers), new ParticleEmitter(subElement))); break; case "particleemitterhitstructure": - ParticleEmitterHitStructure.Add(new ParticleEmitter(subElement)); + particleEmitterHitStructure.Add(new ParticleEmitter(subElement)); break; case "particleemitterhitcharacter": - ParticleEmitterHitCharacter.Add(new ParticleEmitter(subElement)); + particleEmitterHitCharacter.Add(new ParticleEmitter(subElement)); break; } } @@ -67,12 +63,12 @@ namespace Barotrauma.Items.Components partial void UseProjSpecific(float deltaTime, Vector2 raystart) { - if (ParticleEmitter != null) + foreach (ParticleEmitter particleEmitter in particleEmitters) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); - ParticleEmitter.Emit( + particleEmitter.Emit( deltaTime, ConvertUnits.ToDisplayUnits(raystart), - item.CurrentHull, particleAngle, ParticleEmitter.Prefab.CopyEntityAngle ? -particleAngle : 0); + item.CurrentHull, particleAngle, particleEmitter.Prefab.CopyEntityAngle ? -particleAngle : 0); } } @@ -94,7 +90,7 @@ namespace Barotrauma.Items.Components Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); if (targetStructure.Submarine != null) particlePos += targetStructure.Submarine.DrawPosition; - foreach (var emitter in ParticleEmitterHitStructure) + foreach (var emitter in particleEmitterHitStructure) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); @@ -105,7 +101,7 @@ namespace Barotrauma.Items.Components { Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); if (targetCharacter.Submarine != null) particlePos += targetCharacter.Submarine.DrawPosition; - foreach (var emitter in ParticleEmitterHitCharacter) + foreach (var emitter in particleEmitterHitCharacter) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi); @@ -133,7 +129,7 @@ namespace Barotrauma.Items.Components Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); if (targetItem.Submarine != null) particlePos += targetItem.Submarine.DrawPosition; - foreach (var emitter in ParticleEmitterHitItem) + foreach (var emitter in particleEmitterHitItem) { if (!emitter.First.MatchesItem(targetItem)) continue; float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); @@ -141,7 +137,7 @@ namespace Barotrauma.Items.Components } } #if DEBUG - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (GameMain.DebugDraw && IsActive) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs index d47986d28..4925339d3 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs @@ -10,16 +10,10 @@ namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IDrawableComponent { - public GUIButton RepairButton - { - get { return repairButton; } - } - private GUIButton repairButton; - public GUIButton SabotageButton - { - get { return sabotageButton; } - } - private GUIButton sabotageButton; + public GUIButton RepairButton { get; private set; } + + public GUIButton SabotageButton { get; private set; } + private GUIProgressBar progressBar; private List particleEmitters = new List(); @@ -33,7 +27,7 @@ namespace Barotrauma.Items.Components private FixActions requestStartFixAction; - [Serialize("", false)] + [Serialize("", false, description: "An optional description of the needed repairs displayed in the repair interface.")] public string Description { get; @@ -83,7 +77,7 @@ namespace Barotrauma.Items.Components repairButtonText = TextManager.Get("RepairButton"); repairingText = TextManager.Get("Repairing"); - repairButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.TopCenter), repairButtonText) + RepairButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.TopCenter), repairButtonText) { OnClicked = (btn, obj) => { @@ -94,7 +88,7 @@ namespace Barotrauma.Items.Components }; sabotageButtonText = TextManager.Get("SabotageButton"); sabotagingText = TextManager.Get("Sabotaging"); - sabotageButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), sabotageButtonText) + SabotageButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), sabotageButtonText) { OnClicked = (btn, obj) => { @@ -165,14 +159,14 @@ namespace Barotrauma.Items.Components progressBar.BarSize = item.Condition / item.MaxCondition; progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, Color.Red, Color.Orange, Color.Green); - repairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && item.ConditionPercentage <= ShowRepairUIThreshold; - repairButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Repair) ? + RepairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && item.ConditionPercentage <= ShowRepairUIThreshold; + RepairButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Repair) ? repairButtonText : repairingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); - sabotageButton.Visible = character.IsTraitor; - sabotageButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Sabotage)) && character.IsTraitor && item.ConditionPercentage > MinSabotageCondition; - sabotageButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Sabotage || !character.IsTraitor) ? + SabotageButton.Visible = character.IsTraitor; + SabotageButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Sabotage)) && character.IsTraitor && item.ConditionPercentage > MinSabotageCondition; + SabotageButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Sabotage || !character.IsTraitor) ? sabotageButtonText : sabotagingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); @@ -193,7 +187,7 @@ namespace Barotrauma.Items.Components } } - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (GameMain.DebugDraw && Character.Controlled?.FocusedItem == item) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/MotionSensor.cs index 4b4559440..08dd799bc 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/MotionSensor.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Items.Components get { return new Vector2(rangeX, rangeY) * 2.0f; } } - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (!editing || !MapEntity.SelectedList.Contains(item)) return; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/WifiComponent.cs index 0d09c1e2b..99da96f2a 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/WifiComponent.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Items.Components get { return new Vector2(range * 2); } } - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (!editing || !MapEntity.SelectedList.Contains(item)) return; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs index 2d1ca769f..0f47fbbf6 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs @@ -48,7 +48,7 @@ namespace Barotrauma.Items.Components get { return sectionExtents; } } - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (sections.Count == 0 && !IsActive || Hidden) { @@ -154,6 +154,18 @@ namespace Barotrauma.Items.Components public static void UpdateEditing(List wires) { + Wire equippedWire = + Character.Controlled?.SelectedItems[0]?.GetComponent() ?? + Character.Controlled?.SelectedItems[1]?.GetComponent(); + if (equippedWire != null) + { + if (PlayerInput.LeftButtonClicked() && Character.Controlled.SelectedConstruction == null) + { + equippedWire.Use(1.0f, Character.Controlled); + } + return; + } + //dragging a node of some wire if (draggingWire != null) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs index 3b49f50c2..2008e67a4 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs @@ -33,14 +33,14 @@ namespace Barotrauma.Items.Components TextManager.Get("NotBreathing") }; - [Serialize(500.0f, false)] + [Serialize(500.0f, false, 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)] + [Serialize(50.0f, false, description: "The range within which the health info texts fades out.")] public float FadeOutRange { get; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs index 1e5c2c62b..4f203d11d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components private Vector2 crosshairPos, crosshairPointerPos; - private Dictionary widgets = new Dictionary(); + private readonly Dictionary widgets = new Dictionary(); private float prevAngle; private bool flashLowPower; @@ -33,30 +33,30 @@ namespace Barotrauma.Items.Components private float flashTimer; private float flashLength = 1; - private List particleEmitters = new List(); + private readonly List particleEmitters = new List(); - [Editable, Serialize("0.0,0.0,0.0,0.0", true)] + [Editable, Serialize("0,0,0,0", true, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] public Color HudTint { get; private set; } - [Serialize(false, false)] + [Serialize(false, false, 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)] + [Serialize(false, false, 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)] + [Serialize(0.0f, false, description: "How far the barrel \"recoils back\" when the turret is fired (in pixels).")] public float RecoilDistance { get; @@ -240,7 +240,7 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } - public void Draw(SpriteBatch spriteBatch, bool editing = false) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); if (item.Submarine != null) drawPos += item.Submarine.DrawPosition; diff --git a/Barotrauma/BarotraumaClient/Source/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/Source/Items/DockingPort.cs index 4c6e87647..30dcd6401 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/DockingPort.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - public void Draw(SpriteBatch spriteBatch, bool editing) + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (dockingState == 0.0f) return; @@ -105,5 +105,56 @@ namespace Barotrauma.Items.Components } } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + bool isDocked = msg.ReadBoolean(); + + for (int i = 0; i < 2; i++) + { + if (hulls[i] == null) continue; + item.linkedTo.Remove(hulls[i]); + hulls[i].Remove(); + hulls[i] = null; + } + + if (gap != null) + { + item.linkedTo.Remove(gap); + gap.Remove(); + gap = null; + } + + if (isDocked) + { + ushort dockingTargetID = msg.ReadUInt16(); + + bool isLocked = msg.ReadBoolean(); + + Entity targetEntity = Entity.FindEntityByID(dockingTargetID); + if (targetEntity == null || !(targetEntity is Item)) + { + DebugConsole.ThrowError("Invalid docking port network event (can't dock to " + targetEntity?.ToString() ?? "null" + ")"); + return; + } + + DockingTarget = (targetEntity as Item).GetComponent(); + if (DockingTarget == null) + { + DebugConsole.ThrowError("Invalid docking port network event (" + targetEntity + " doesn't have a docking port component)"); + return; + } + + Dock(DockingTarget); + + if (isLocked) + { + Lock(isNetworkMessage: true, forcePosition: true); + } + } + else + { + Undock(); + } + } } } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs index f12656960..2ca75ce47 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs @@ -209,7 +209,14 @@ namespace Barotrauma public static SlotReference SelectedSlot { - get { return selectedSlot; } + get + { + if (selectedSlot?.ParentInventory?.Owner == null || selectedSlot.ParentInventory.Owner.Removed) + { + return null; + } + return selectedSlot; + } } public virtual void CreateSlots() @@ -430,6 +437,8 @@ namespace Barotrauma if (canMove) { + subInventory.HideTimer = 1.0f; + subInventory.OpenState = 1.0f; if (subInventory.movableFrameRect.Contains(PlayerInput.MousePosition) && PlayerInput.RightButtonClicked()) { container.Inventory.savedPosition = container.Inventory.originalPos; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Item.cs b/Barotrauma/BarotraumaClient/Source/Items/Item.cs index 2b4cceb29..13a582b24 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Item.cs @@ -26,6 +26,9 @@ namespace Barotrauma public float LastImpactSoundTime; public const float ImpactSoundInterval = 0.2f; + private bool editingHUDRefreshPending; + private float editingHUDRefreshTimer; + class SpriteState { public float RotationState; @@ -33,7 +36,7 @@ namespace Barotrauma public bool IsActive = true; } - private Dictionary spriteAnimState = new Dictionary(); + private Dictionary spriteAnimState = new Dictionary(); private Sprite activeSprite; public override Sprite Sprite @@ -128,11 +131,13 @@ namespace Barotrauma } } - foreach (BrokenItemSprite brokenSprite in Prefab.BrokenSprites) + for (int i = 0; i < Prefab.BrokenSprites.Count;i++) { - if (condition <= brokenSprite.MaxCondition) + float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxCondition : 0.0f; + if (condition <= minCondition || + condition <= Prefab.BrokenSprites[i].MaxCondition && !Prefab.BrokenSprites[i].FadeIn) { - activeSprite = brokenSprite.Sprite; + activeSprite = Prefab.BrokenSprites[i].Sprite; break; } } @@ -211,18 +216,18 @@ namespace Barotrauma } } + float depth = GetDrawDepth(); if (activeSprite != null) { SpriteEffects oldEffects = activeSprite.effects; activeSprite.effects ^= SpriteEffects; SpriteEffects oldBrokenSpriteEffects = SpriteEffects.None; - if (fadeInBrokenSprite != null) + if (fadeInBrokenSprite != null && fadeInBrokenSprite.Sprite != activeSprite) { oldBrokenSpriteEffects = fadeInBrokenSprite.Sprite.effects; fadeInBrokenSprite.Sprite.effects ^= SpriteEffects; } - float depth = GetDrawDepth(); if (body == null) { bool flipHorizontal = (SpriteEffects & SpriteEffects.FlipHorizontally) != 0; @@ -304,9 +309,9 @@ namespace Barotrauma } activeSprite.effects = oldEffects; - if (fadeInBrokenSprite != null) + if (fadeInBrokenSprite != null && fadeInBrokenSprite.Sprite != activeSprite) { - fadeInBrokenSprite.Sprite.effects = oldEffects; + fadeInBrokenSprite.Sprite.effects = oldBrokenSpriteEffects; } } @@ -314,7 +319,7 @@ namespace Barotrauma //causing them to be removed from the list for (int i = drawableComponents.Count - 1; i >= 0; i--) { - drawableComponents[i].Draw(spriteBatch, editing); + drawableComponents[i].Draw(spriteBatch, editing, depth); } if (GameMain.DebugDraw) @@ -437,21 +442,23 @@ namespace Barotrauma public override void UpdateEditing(Camera cam) { - if (editingHUD == null || editingHUD.UserData as Item != this) + if (editingHUD == null || editingHUD.UserData as Item != this || + (editingHUDRefreshPending && editingHUDRefreshTimer <= 0.0f)) { editingHUD = CreateEditingHUD(Screen.Selected != GameMain.SubEditorScreen); + editingHUDRefreshTimer = 1.0f; } - if (Screen.Selected != GameMain.SubEditorScreen) return; + if (Screen.Selected != GameMain.SubEditorScreen) { return; } - if (Character.Controlled == null) activeHUDs.Clear(); + if (Character.Controlled == null) { activeHUDs.Clear(); } - if (!Linkable) return; + if (!Linkable) { return; } - if (!PlayerInput.KeyDown(Keys.Space)) return; + if (!PlayerInput.KeyDown(Keys.Space)) { return; } bool lClick = PlayerInput.LeftButtonClicked(); bool rClick = PlayerInput.RightButtonClicked(); - if (!lClick && !rClick) return; + if (!lClick && !rClick) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); var otherEntity = mapEntityList.FirstOrDefault(e => e != this && e.IsHighlighted && e.IsMouseOn(position)); @@ -472,6 +479,8 @@ namespace Barotrauma public GUIComponent CreateEditingHUD(bool inGame = false) { + editingHUDRefreshPending = false; + int heightScaled = (int)(20 * GUI.Scale); editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null) @@ -672,6 +681,13 @@ namespace Barotrauma editingHUDCreated = editingHUD != null && editingHUD != prevEditingHUD; } + if (editingHUD == null || + !(GUI.KeyboardDispatcher.Subscriber is GUITextBox textBox) || + !editingHUD.IsParentOf(textBox)) + { + editingHUDRefreshTimer -= deltaTime; + } + List prevActiveHUDs = new List(activeHUDs); List activeComponents = new List(components); foreach (MapEntity entity in linkedTo) @@ -916,6 +932,7 @@ namespace Barotrauma break; case NetEntityEvent.Type.ChangeProperty: ReadPropertyChange(msg, false); + editingHUDRefreshPending = true; break; case NetEntityEvent.Type.Invalid: break; @@ -954,6 +971,7 @@ namespace Barotrauma break; case NetEntityEvent.Type.ChangeProperty: WritePropertyChange(msg, extraData, true); + editingHUDRefreshTimer = 1.0f; break; case NetEntityEvent.Type.Combine: UInt16 combineTargetID = (UInt16)extraData[1]; @@ -969,6 +987,7 @@ namespace Barotrauma if (parentInventory != null || body == null || !body.Enabled || Removed) { + positionBuffer.Clear(); return; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/Source/Items/ItemPrefab.cs index d57d32a62..84ba9b12d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/ItemPrefab.cs @@ -47,138 +47,6 @@ namespace Barotrauma partial class ItemPrefab : MapEntityPrefab { - public class DecorativeSprite - { - public Sprite Sprite { get; private set; } - - public enum AnimationType - { - None, - Sine, - Noise - } - - [Serialize("0,0", false)] - public Vector2 Offset { get; private set; } - - [Serialize(AnimationType.None, false)] - public AnimationType OffsetAnim { get; private set; } - - [Serialize(0.0f, false)] - public float OffsetAnimSpeed { get; private set; } - - private float rotationSpeedRadians; - [Serialize(0.0f, false)] - public float RotationSpeed - { - get - { - return MathHelper.ToDegrees(rotationSpeedRadians); - } - private set - { - rotationSpeedRadians = MathHelper.ToRadians(value); - } - } - - [Serialize(0.0f, false)] - public float Rotation { get; private set; } - - [Serialize(AnimationType.None, false)] - public AnimationType RotationAnim { get; private set; } - - /// - /// If > 0, only one sprite of the same group is used (chosen randomly) - /// - [Serialize(0, false)] - public int RandomGroupID { get; private set; } - - /// - /// The sprite is only drawn if these conditions are fulfilled - /// - public List IsActiveConditionals { get; private set; } = new List(); - /// - /// The sprite is only animated if these conditions are fulfilled - /// - public List AnimationConditionals { get; private set; } = new List(); - - public DecorativeSprite(XElement element, string path = "", bool lazyLoad = false) - { - Sprite = new Sprite(element, path, lazyLoad: lazyLoad); - SerializableProperty.DeserializeProperties(this, element); - - foreach (XElement subElement in element.Elements()) - { - List conditionalList = null; - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "conditional": - case "isactiveconditional": - conditionalList = IsActiveConditionals; - break; - case "animationconditional": - conditionalList = AnimationConditionals; - break; - default: - continue; - } - foreach (XAttribute attribute in subElement.Attributes()) - { - if (attribute.Name.ToString().ToLowerInvariant() == "targetitemcomponent") { continue; } - conditionalList.Add(new PropertyConditional(attribute)); - } - } - } - - public Vector2 GetOffset(ref float offsetState) - { - if (OffsetAnimSpeed <= 0.0f) - { - return Offset; - } - switch (OffsetAnim) - { - case AnimationType.Sine: - offsetState = offsetState % (MathHelper.TwoPi / OffsetAnimSpeed); - return Offset * (float)Math.Sin(offsetState * OffsetAnimSpeed); - case AnimationType.Noise: - offsetState = offsetState % (1.0f / (OffsetAnimSpeed * 0.1f)); - - float t = offsetState * 0.1f * OffsetAnimSpeed; - return new Vector2( - Offset.X * (PerlinNoise.GetPerlin(t, t) - 0.5f), - Offset.Y * (PerlinNoise.GetPerlin(t + 0.5f, t + 0.5f) - 0.5f)); - default: - return Offset; - } - } - - public float GetRotation(ref float rotationState) - { - if (rotationSpeedRadians <= 0.0f) - { - return Rotation; - } - switch (OffsetAnim) - { - case AnimationType.Sine: - rotationState = rotationState % (MathHelper.TwoPi / rotationSpeedRadians); - return Rotation * (float)Math.Sin(rotationState * rotationSpeedRadians); - case AnimationType.Noise: - rotationState = rotationState % (1.0f / rotationSpeedRadians); - return Rotation * PerlinNoise.GetPerlin(rotationState * rotationSpeedRadians, rotationState * rotationSpeedRadians); - default: - return rotationState * rotationSpeedRadians; - } - } - - public void Remove() - { - Sprite?.Remove(); - Sprite = null; - } - } - public List BrokenSprites = new List(); public List DecorativeSprites = new List(); public List ContainedSprites = new List(); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Rope.cs b/Barotrauma/BarotraumaClient/Source/Items/Rope.cs index 5cba54afb..79f53dc33 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Rope.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Rope.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - public void Draw(SpriteBatch spriteBatch, bool editing = false) + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (!IsActive) return; diff --git a/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index 881b9beb9..7b8818189 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -22,6 +22,7 @@ namespace Barotrauma { LoadConfig(configPath); } + public BackgroundCreatureManager(IEnumerable files) { foreach(var file in files) @@ -29,14 +30,26 @@ namespace Barotrauma LoadConfig(file); } } + private void LoadConfig(string configPath) { try { XDocument doc = XMLExtensions.TryLoadXml(configPath); - if (doc == null || doc.Root == null) return; + if (doc == null) { return; } + var mainElement = doc.Root; + if (mainElement.IsOverride()) + { + mainElement = doc.Root.FirstElement(); + prefabs.Clear(); + DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.Yellow); + } + else if (prefabs.Any()) + { + DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'"); + } - foreach (XElement element in doc.Root.Elements()) + foreach (XElement element in mainElement.Elements()) { prefabs.Add(new BackgroundCreaturePrefab(element)); }; @@ -46,6 +59,7 @@ namespace Barotrauma DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e); } } + public void SpawnSprites(int count, Vector2? position = null) { activeSprites.Clear(); diff --git a/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs b/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs index 6ea7d06f2..72d5db102 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs @@ -41,7 +41,7 @@ namespace Barotrauma { if (subElement.Name.ToString().ToLowerInvariant() != "sprite") continue; - Sprite = new Sprite(subElement); + Sprite = new Sprite(subElement, lazyLoad: true); break; } } diff --git a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs index 8a2b66f02..e8ca4f341 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs @@ -132,7 +132,7 @@ namespace Barotrauma var newDeformation = SpriteDeformation.Load(animationElement, Prefab.Name); if (newDeformation != null) { - newDeformation.DeformationParams = Prefab.SpriteDeformations[j].DeformationParams; + newDeformation.Params = Prefab.SpriteDeformations[j].Params; spriteDeformations.Add(newDeformation); j++; } diff --git a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs index da2bc00a6..97146d94a 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs @@ -72,6 +72,11 @@ namespace Barotrauma.Lights private float ambientLightUpdateTimer; + public IEnumerable Lights + { + get { return lights; } + } + public LightManager(GraphicsDevice graphics, ContentManager content) { lights = new List(); diff --git a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightSource.cs index 946001c44..eadd2a93c 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightSource.cs @@ -10,15 +10,11 @@ namespace Barotrauma.Lights { class LightSourceParams : ISerializableEntity { - public string Name => "LightSource"; + public string Name => "Light Source"; 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), Editable] public Color Color @@ -28,6 +24,7 @@ namespace Barotrauma.Lights } private float range; + [Serialize(100.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { @@ -64,7 +61,7 @@ namespace Barotrauma.Lights public LightSourceParams(XElement element) { - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + Deserialize(element); foreach (XElement subElement in element.Elements()) { @@ -103,6 +100,17 @@ namespace Barotrauma.Lights Range = range; Color = color; } + + public bool Deserialize(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + return SerializableProperties != null; + } + + public void Serialize(XElement element) + { + SerializableProperty.SerializeProperties(this, element, true); + } } class LightSource @@ -893,13 +901,21 @@ namespace Barotrauma.Lights if (GameMain.DebugDraw) { + Vector2 drawPos = position; + if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } + drawPos.Y = -drawPos.Y; + + if (CastShadows && Screen.Selected == GameMain.SubEditorScreen) + { + GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 20, Vector2.One * 40, Color.Orange, isFilled: false); + GUI.DrawLine(spriteBatch, drawPos - Vector2.One * 20, drawPos + Vector2.One * 20, Color.Orange); + GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * 20, drawPos + new Vector2(1.0f, -1.0f) * 20, Color.Orange); + } + //visualize light recalculations float timeSinceRecalculation = (float)Timing.TotalTime - lastRecalculationTime; if (timeSinceRecalculation < 0.1f) { - Vector2 drawPos = position; - if (ParentSub != null) drawPos += ParentSub.DrawPosition; - drawPos.Y = -drawPos.Y; GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, Color.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/Source/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs index cb136c4b0..c0edd053d 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs @@ -92,7 +92,7 @@ namespace Barotrauma public virtual void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { } - public virtual void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect) { } + public virtual void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect, bool editing) { } /// /// Update the selection logic in submarine editor diff --git a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs index 6f18d50c0..b4f2bf2d4 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs @@ -170,9 +170,9 @@ namespace Barotrauma Draw(spriteBatch, editing, back, null); } - public override void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect) + public override void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect, bool editing) { - Draw(spriteBatch, false, false, damageEffect); + Draw(spriteBatch, editing, false, damageEffect); } private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) @@ -265,7 +265,7 @@ namespace Barotrauma } } - if (back == depth > 0.5f || editing) + if (back == depth > 0.5f) { SpriteEffects oldEffects = prefab.sprite.effects; prefab.sprite.effects ^= SpriteEffects; diff --git a/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs b/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs index 80f6ee878..9d859adf5 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs @@ -235,7 +235,7 @@ namespace Barotrauma foreach (MapEntity e in entitiesToRender) { if (e.DrawDamageEffect) - e.DrawDamage(spriteBatch, damageEffect); + e.DrawDamage(spriteBatch, damageEffect, editing); } if (damageEffect != null) { diff --git a/Barotrauma/BarotraumaClient/Source/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/Source/Map/WayPoint.cs index c14813c0b..5b5efd455 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/WayPoint.cs @@ -301,7 +301,7 @@ namespace Barotrauma } }; jobDropDown.AddItem(TextManager.Get("Any"), null); - foreach (JobPrefab jobPrefab in JobPrefab.List) + foreach (JobPrefab jobPrefab in JobPrefab.List.Values) { jobDropDown.AddItem(jobPrefab.Name, jobPrefab); } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Client.cs b/Barotrauma/BarotraumaClient/Source/Networking/Client.cs index 4290225b6..2d6617091 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Client.cs @@ -9,6 +9,7 @@ namespace Barotrauma.Networking struct TempClient { public string Name; + public UInt16 NameID; public UInt64 SteamID; public byte ID; public UInt16 CharacterID; diff --git a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs index 7dca12e5e..83fdc588f 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs @@ -18,6 +18,22 @@ namespace Barotrauma.Networking get { return true; } } + private string name; + + private UInt16 nameId = 0; + + public string Name + { + get { return name; } + } + + public void SetName(string value) + { + if (string.IsNullOrEmpty(value)) { return; } + name = value.Replace(":", "").Replace(";", ""); + nameId++; + } + private ClientPeer clientPeer; public ClientPeer ClientPeer { get { return clientPeer; } } @@ -213,7 +229,7 @@ namespace Barotrauma.Networking Hull.EditFire = false; Hull.EditWater = false; - Name = newName; + SetName(newName); entityEventManager = new ClientEntityEventManager(this); @@ -221,7 +237,7 @@ namespace Barotrauma.Networking fileReceiver.OnFinished += OnFileReceived; fileReceiver.OnTransferFailed += OnTransferFailed; - characterInfo = new CharacterInfo(Character.HumanConfigFile, name, null) + characterInfo = new CharacterInfo(Character.HumanSpeciesName, name, null) { Job = null }; @@ -311,6 +327,7 @@ namespace Barotrauma.Networking translatedEndpoint = endpoint; } clientPeer.OnDisconnect = OnDisconnect; + clientPeer.OnDisconnectMessageReceived = HandleDisconnectMessage; clientPeer.OnInitializationComplete = () => { if (SteamManager.IsInitialized) @@ -323,6 +340,8 @@ namespace Barotrauma.Networking canStart = true; connected = true; + VoipClient = new VoipClient(this, clientPeer); + if (Screen.Selected != GameMain.GameScreen) { GameMain.NetLobbyScreen.Select(); @@ -591,13 +610,17 @@ namespace Barotrauma.Networking respawnManager.Update(deltaTime); } - if (updateTimer > DateTime.Now) { return; } - SendIngameUpdate(); + if (updateTimer <= DateTime.Now) + { + SendIngameUpdate(); + } } else { - if (updateTimer > DateTime.Now) { return; } - SendLobbyUpdate(); + if (updateTimer <= DateTime.Now) + { + SendLobbyUpdate(); + } } if (serverSettings.VoiceChatEnabled) @@ -615,8 +638,11 @@ namespace Barotrauma.Networking } } - // Update current time - updateTimer = DateTime.Now + updateInterval; + if (updateTimer <= DateTime.Now) + { + // Update current time + updateTimer = DateTime.Now + updateInterval; + } } private CoroutineHandle startGameCoroutine; @@ -641,6 +667,9 @@ namespace Barotrauma.Networking { errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace; } +#if DEBUG + DebugConsole.ThrowError("Error while reading an ingame update message from server.", e); +#endif GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); throw; } @@ -671,10 +700,21 @@ namespace Barotrauma.Networking IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); + MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? + GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; + GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; - bool readyToStart = - GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList) && - GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); + bool readyToStart; + if (campaign == null) + { + readyToStart = GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList) && + GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); + } + else + { + readyToStart = !fileReceiver.ActiveTransfers.Any(c => c.FileType == FileTransferType.CampaignSave) && + (campaign.LastSaveID == campaign.PendingSaveID); + } readyToStartMsg.Write(readyToStart); WriteCharacterInfo(readyToStartMsg); @@ -741,18 +781,16 @@ namespace Barotrauma.Networking } } - private void OnDisconnect(string disconnectMsg) - { - HandleDisconnectMessage(disconnectMsg); - } - - private void HandleDisconnectMessage(string disconnectMsg) + private void OnDisconnect() { if (SteamManager.IsInitialized) { SteamManager.Instance.User.ClearRichPresence(); } + } + private void HandleDisconnectMessage(string disconnectMsg) + { disconnectMsg = disconnectMsg ?? ""; string[] splitMsg = disconnectMsg.Split('/'); @@ -910,9 +948,13 @@ namespace Barotrauma.Networking private void ReadTraitorMessage(IReadMessage inc) { TraitorMessageType messageType = (TraitorMessageType)inc.ReadByte(); + string missionIdentifier = inc.ReadString(); string message = inc.ReadString(); message = TextManager.GetServerMessage(message); + var missionPrefab = TraitorMissionPrefab.List.Find(t => t.Identifier == missionIdentifier); + Sprite icon = missionPrefab?.Icon; + switch(messageType) { case TraitorMessageType.Objective: var isTraitor = !string.IsNullOrEmpty(message); @@ -932,7 +974,11 @@ namespace Barotrauma.Networking DebugConsole.NewMessage(message); break; case TraitorMessageType.ServerMessageBox: - new GUIMessageBox("", message); + var msgBox = new GUIMessageBox("", message, new string[0], type: GUIMessageBox.Type.InGame, icon: icon); + if (msgBox.Icon != null) + { + msgBox.IconColor = missionPrefab.IconColor; + } break; case TraitorMessageType.Server: default: @@ -1137,6 +1183,12 @@ namespace Barotrauma.Networking mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); } + if (GameMain.GameSession.Submarine.IsFileCorrupted) + { + DebugConsole.ThrowError($"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Name}\"."); + yield return CoroutineStatus.Failure; + } + for (int i = 0; i < Submarine.MainSubs.Length; i++) { if (!loadSecondSub && i > 0) { break; } @@ -1221,7 +1273,6 @@ namespace Barotrauma.Networking private void ReadInitialUpdate(IReadMessage inc) { myID = inc.ReadByte(); - VoipClient = new VoipClient(this, clientPeer); UInt16 subListCount = inc.ReadUInt16(); serverSubmarines.Clear(); @@ -1229,16 +1280,14 @@ namespace Barotrauma.Networking { string subName = inc.ReadString(); string subHash = inc.ReadString(); + bool requiredContentPackagesInstalled = inc.ReadBoolean(); - var matchingSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); - if (matchingSub != null) - { - serverSubmarines.Add(matchingSub); - } - else - { - serverSubmarines.Add(new Submarine(Path.Combine(Submarine.SavePath, subName) + ".sub", subHash, false)); - } + var matchingSub = + Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash) ?? + new Submarine(Path.Combine(Submarine.SavePath, subName) + ".sub", subHash, false); + + matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled; + serverSubmarines.Add(matchingSub); } GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, serverSubmarines); @@ -1265,6 +1314,7 @@ namespace Barotrauma.Networking { byte id = inc.ReadByte(); UInt64 steamId = inc.ReadUInt64(); + UInt16 nameId = inc.ReadUInt16(); string name = inc.ReadString(); UInt16 characterID = inc.ReadUInt16(); bool muted = inc.ReadBoolean(); @@ -1274,6 +1324,7 @@ namespace Barotrauma.Networking tempClients.Add(new TempClient { ID = id, + NameID = nameId, SteamID = steamId, Name = name, CharacterID = characterID, @@ -1301,6 +1352,7 @@ namespace Barotrauma.Networking ConnectedClients.Add(existingClient); GameMain.NetLobbyScreen.AddPlayer(existingClient); } + existingClient.NameID = tc.NameID; existingClient.Character = null; existingClient.Muted = tc.Muted; existingClient.AllowKicking = tc.AllowKicking; @@ -1315,7 +1367,11 @@ namespace Barotrauma.Networking if (existingClient.ID == myID) { existingClient.SetPermissions(permissions, permittedConsoleCommands); - name = tc.Name; + if (!NetIdUtils.IdMoreRecent(nameId, tc.NameID)) + { + name = tc.Name; + nameId = tc.NameID; + } if (GameMain.NetLobbyScreen.CharacterNameBox != null && !GameMain.NetLobbyScreen.CharacterNameBox.Selected) { @@ -1591,6 +1647,7 @@ namespace Barotrauma.Networking outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); outmsg.Write(ChatMessage.LastID); outmsg.Write(LastClientListUpdateID); + outmsg.Write(nameId); outmsg.Write(name); var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; @@ -1795,6 +1852,7 @@ namespace Barotrauma.Networking } SaveUtil.LoadGame(GameMain.GameSession.SavePath, GameMain.GameSession); + GameMain.GameSession?.Submarine?.CheckSubsLeftBehind(); campaign.LastSaveID = campaign.PendingSaveID; DebugConsole.Log("Campaign save received, save ID " + campaign.LastSaveID); @@ -2123,7 +2181,15 @@ namespace Barotrauma.Networking public bool SpectateClicked(GUIButton button, object userData) { - if (button != null) button.Enabled = false; + MultiPlayerCampaign campaign = + GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? + GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; + if (campaign != null && campaign.LastSaveID < campaign.PendingSaveID) + { + new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress")); + return false; + } + if (button != null) { button.Enabled = false; } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/ClientPeer.cs index 2e18f01bd..587436398 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/ClientPeer.cs @@ -7,12 +7,14 @@ namespace Barotrauma.Networking abstract class ClientPeer { public delegate void MessageCallback(IReadMessage message); - public delegate void DisconnectCallback(string msg); + public delegate void DisconnectCallback(); + public delegate void DisconnectMessageCallback(string message); public delegate void PasswordCallback(int salt, int retries); public delegate void InitializationCompleteCallback(); public MessageCallback OnMessageReceived; public DisconnectCallback OnDisconnect; + public DisconnectMessageCallback OnDisconnectMessageReceived; public PasswordCallback OnRequestPassword; public InitializationCompleteCallback OnInitializationComplete; diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs index 508499295..8835b49e9 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -136,6 +136,7 @@ namespace Barotrauma.Networking case NetConnectionStatus.Disconnected: string disconnectMsg = inc.ReadString(); Close(disconnectMsg); + OnDisconnectMessageReceived?.Invoke(disconnectMsg); break; } } @@ -227,7 +228,7 @@ namespace Barotrauma.Networking netClient.Shutdown(msg ?? TextManager.Get("Disconnecting")); netClient = null; steamAuthTicket?.Cancel(); steamAuthTicket = null; - OnDisconnect?.Invoke(msg); + OnDisconnect?.Invoke(); } public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) @@ -248,6 +249,13 @@ namespace Barotrauma.Networking break; } +#if DEBUG + netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Client.SimulatedDuplicatesChance; + netPeerConfiguration.SimulatedMinimumLatency = GameMain.Client.SimulatedMinimumLatency; + netPeerConfiguration.SimulatedRandomLatency = GameMain.Client.SimulatedRandomLatency; + netPeerConfiguration.SimulatedLoss = GameMain.Client.SimulatedLoss; +#endif + NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); byte[] msgData = new byte[msg.LengthBytes]; msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs index aa913f20a..ea4376fef 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -116,6 +116,7 @@ namespace Barotrauma.Networking IReadMessage inc = new ReadOnlyMessage(data, false, 1, dataLength - 1, ServerConnection); string msg = inc.ReadString(); Close(msg); + OnDisconnectMessageReceived?.Invoke(msg); } else { @@ -270,8 +271,27 @@ namespace Barotrauma.Networking } heartbeatTimer = 5.0; - bool successSend = SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, buf, length + 4, sendType); +#if DEBUG + CoroutineManager.InvokeAfter(() => + { + if (Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedLoss && sendType != Facepunch.Steamworks.Networking.SendType.Reliable) { return; } + int count = Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedDuplicatesChance ? 2 : 1; + for (int i = 0; i < count; i++) + { + Send(buf, length + 4, sendType); + } + }, + GameMain.Client.SimulatedMinimumLatency + Rand.Range(0.0f, GameMain.Client.SimulatedRandomLatency)); + +#else + Send(buf, length + 4, sendType); +#endif + } + + private void Send(byte[] buf, int length, Facepunch.Steamworks.Networking.SendType sendType) + { + bool successSend = SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, buf, length + 4, sendType); if (!successSend) { if (sendType != Facepunch.Steamworks.Networking.SendType.Reliable) @@ -332,7 +352,7 @@ namespace Barotrauma.Networking steamAuthTicket?.Cancel(); steamAuthTicket = null; hostSteamId = 0; - OnDisconnect?.Invoke(msg); + OnDisconnect?.Invoke(); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index bb306eac3..06898e409 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -414,6 +414,7 @@ namespace Barotrauma.Networking case NetConnectionStatus.Disconnected: string disconnectMsg = inc.ReadString(); Close(disconnectMsg); + OnDisconnectMessageReceived?.Invoke(disconnectMsg); break; } } @@ -429,7 +430,7 @@ namespace Barotrauma.Networking isActive = false; - for (int i=remotePeers.Count-1;i>=0;i--) + for (int i = remotePeers.Count - 1; i >= 0; i--) { DisconnectPeer(remotePeers[i], msg ?? DisconnectReason.ServerShutdown.ToString()); } @@ -444,7 +445,7 @@ namespace Barotrauma.Networking netClient.Shutdown(msg ?? TextManager.Get("Disconnecting")); netClient = null; - OnDisconnect?.Invoke(msg); + OnDisconnect?.Invoke(); Steam.SteamManager.Instance.Networking.OnIncomingConnection = null; Steam.SteamManager.Instance.Networking.OnP2PData = null; @@ -478,6 +479,12 @@ namespace Barotrauma.Networking lidgrenMsg.Write((UInt16)length); lidgrenMsg.Write(msgData, 0, length); +#if DEBUG + netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Client.SimulatedDuplicatesChance; + netPeerConfiguration.SimulatedMinimumLatency = GameMain.Client.SimulatedMinimumLatency; + netPeerConfiguration.SimulatedRandomLatency = GameMain.Client.SimulatedRandomLatency; + netPeerConfiguration.SimulatedLoss = GameMain.Client.SimulatedLoss; +#endif NetSendResult result = netClient.SendMessage(lidgrenMsg, lidgrenDeliveryMethod); if (result != NetSendResult.Queued && result != NetSendResult.Sent) { diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs index b8b849359..bf9e4b337 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs @@ -758,8 +758,14 @@ namespace Barotrauma.Networking karmaSettingsBlocker = new GUIFrame(new RectTransform(Vector2.One, karmaSettingsContainer.RectTransform, Anchor.CenterLeft) { MaxSize = new Point(karmaSettingsList.Content.Rect.Width, int.MaxValue) }, style: "InnerFrame"); + karmaPresetDD.SelectItem(KarmaPreset); + karmaSettingsBlocker.Visible = !karmaBox.Selected || KarmaPreset != "custom"; + GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); karmaPresetDD.OnSelected = (selected, obj) => { + string newKarmaPreset = obj as string; + if (newKarmaPreset == KarmaPreset) { return true; } + List properties = netProperties.Values.ToList(); List prevValues = new List(); foreach (NetPropertyData prop in netProperties.Values) @@ -772,7 +778,7 @@ namespace Barotrauma.Networking GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); GameMain.NetworkMember?.KarmaManager?.Save(); } - KarmaPreset = obj as string; + KarmaPreset = newKarmaPreset; GameMain.NetworkMember.KarmaManager.SelectPreset(KarmaPreset); karmaSettingsList.Content.ClearChildren(); karmaSettingsBlocker.Visible = !karmaBox.Selected || KarmaPreset != "custom"; @@ -783,7 +789,6 @@ namespace Barotrauma.Networking } return true; }; - karmaPresetDD.SelectItem(KarmaPreset); AssignGUIComponent("KarmaPreset", karmaPresetDD); karmaBox.OnSelected = (tb) => { @@ -836,16 +841,16 @@ namespace Barotrauma.Networking public bool ToggleSettingsFrame(GUIButton button, object obj) { if (settingsFrame == null) + { + CreateSettingsFrame(); + } + else { if (KarmaPreset == "custom") { GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); GameMain.NetworkMember?.KarmaManager?.Save(); } - CreateSettingsFrame(); - } - else - { ClientAdminWrite(NetFlags.Properties); foreach (NetPropertyData prop in netProperties.Values) { diff --git a/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs index c9a7d687b..ca17ec9db 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs @@ -736,14 +736,14 @@ namespace Barotrauma.Steam /// /// 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 void CreateWorkshopItemStaging(Workshop.Item existingItem, out Workshop.Editor itemEditor, out ContentPackage contentPackage) + public static bool CreateWorkshopItemStaging(Workshop.Item existingItem, out Workshop.Editor itemEditor, out ContentPackage contentPackage) { if (!existingItem.Installed) { itemEditor = null; contentPackage = null; DebugConsole.ThrowError("Cannot edit the workshop item \"" + existingItem.Title + "\" because it has not been installed."); - return; + return false; } itemEditor = instance.client.Workshop.EditItem(existingItem.Id); @@ -763,7 +763,7 @@ namespace Barotrauma.Steam TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { existingItem.Title, errorMsg })); itemEditor = null; contentPackage = null; - return; + return false; } } @@ -773,7 +773,7 @@ namespace Barotrauma.Steam if (contentPackage == null && tempContentPackage.GameVersion <= new Version(0, 9, 1, 0)) { - //try finding the content package in the lega + //try finding the content package from the non-legacy path installedContentPackagePath = Path.GetFullPath(GetWorkshopItemContentPackagePath(tempContentPackage, legacy: false)); contentPackage = ContentPackage.List.Find(cp => Path.GetFullPath(cp.Path) == installedContentPackagePath); } @@ -792,8 +792,11 @@ namespace Barotrauma.Steam contentPackage.Path = newPath; itemEditor.Folder = newDir; if (!Directory.Exists(newDir)) { Directory.CreateDirectory(newDir); } - if (File.Exists(newPath)) { File.Delete(newPath); } - File.Move(installedContentPackagePath, newPath); + if (Path.GetFullPath(newPath) != installedContentPackagePath) + { + if (File.Exists(newPath)) { File.Delete(newPath); } + File.Move(installedContentPackagePath, newPath); + } //move all files inside the Mods folder foreach (ContentFile cf in contentPackage.Files) { @@ -815,7 +818,7 @@ namespace Barotrauma.Steam string errorMsg = TextManager.GetWithVariable("WorkshopErrorOnEnable", "[itemname]", TextManager.EnsureUTF8(existingItem.Title)); new GUIMessageBox(TextManager.Get("Error"), errorMsg); DebugConsole.ThrowError(errorMsg, e); - return; + return false; } } @@ -846,6 +849,7 @@ namespace Barotrauma.Steam GameAnalyticsManager.AddErrorEventOnce("SteamManager.CreateWorkshopItemStaging:WriteAllBytesFailed" + previewImagePath, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); } + return true; } public static void StartPublishItem(ContentPackage contentPackage, Workshop.Editor item) @@ -865,11 +869,9 @@ namespace Barotrauma.Steam 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.Folder, PreviewImageName)), PreviewImageName); - item.PreviewImage = Path.GetFullPath(PreviewImageName); + + string previewImagePath = Path.GetFullPath(Path.Combine(item.Folder, PreviewImageName)); + item.PreviewImage = File.Exists(previewImagePath) ? previewImagePath : null; CoroutineManager.StartCoroutine(PublishItem(item)); } @@ -904,7 +906,7 @@ namespace Barotrauma.Steam /// /// Enables a workshop item by moving it to the game folder. /// - public static bool EnableWorkShopItem(Workshop.Item item, bool allowFileOverwrite, out string errorMsg) + public static bool EnableWorkShopItem(Workshop.Item item, bool allowFileOverwrite, out string errorMsg, bool selectContentPackage = true) { if (!item.Installed) { @@ -959,19 +961,24 @@ namespace Barotrauma.Steam } newPackage.Save(newContentPackagePath); ContentPackage.List.Add(newPackage); - if (newPackage.CorePackage) + + if (selectContentPackage) { - //if enabling a core package, disable all other core packages - GameMain.Config.SelectedContentPackages.RemoveWhere(cp => cp.CorePackage); + if (newPackage.CorePackage) + { + //if enabling a core package, disable all other core packages + GameMain.Config.SelectedContentPackages.RemoveAll(cp => cp.CorePackage); + } + GameMain.Config.SelectContentPackage(newPackage); + GameMain.Config.SaveNewPlayerConfig(); + foreach (ContentFile cf in newPackage.Files) + { + if (cf.Type == ContentType.Submarine) + { + Submarine.RefreshSavedSub(cf.Path); + } + } } - GameMain.Config.SelectedContentPackages.Add(newPackage); - GameMain.Config.SaveNewPlayerConfig(); - - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) - { - Submarine.RefreshSavedSubs(); - } - errorMsg = ""; return true; } @@ -1166,7 +1173,8 @@ namespace Barotrauma.Steam } ContentPackage.List.RemoveAll(cp => System.IO.Path.GetFullPath(cp.Path) == System.IO.Path.GetFullPath(installedContentPackagePath)); - GameMain.Config.SelectedContentPackages.RemoveWhere(cp => !ContentPackage.List.Contains(cp)); + GameMain.Config.SelectedContentPackages.RemoveAll(cp => !ContentPackage.List.Contains(cp)); + ContentPackage.SortContentPackages(); GameMain.Config.SaveNewPlayerConfig(); } catch (Exception e) @@ -1218,7 +1226,7 @@ namespace Barotrauma.Steam { metaDataPath = Path.Combine(item.Directory.FullName, MetadataFileName); } - catch (ArgumentException e) + catch (ArgumentException) { string errorMessage = "Metadata file for the Workshop item \"" + item.Title + "\" not found. Could not combine path (" + (item.Directory.FullName ?? "directory name empty") + ")."; @@ -1255,7 +1263,7 @@ namespace Barotrauma.Steam public static bool CheckWorkshopItemUpToDate(Workshop.Item item) { - if (!item.Installed) return false; + if (!item.Installed) { return false; } string metaDataPath = Path.Combine(item.Directory.FullName, MetadataFileName); if (!File.Exists(metaDataPath)) @@ -1274,6 +1282,22 @@ namespace Barotrauma.Steam return item.Modified <= myPackage.InstallTime.Value; } + + public static bool CheckWorkshopItemSelected(Workshop.Item item) + { + if (!item.Installed) { return false; } + + string metaDataPath = Path.Combine(item.Directory.FullName, 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); + return GameMain.Config.SelectedContentPackages.Any(cp => cp.Name == steamPackage.Name); + } + public static bool AutoUpdateWorkshopItems() { if (instance == null || !instance.isInitialized) { return false; } @@ -1290,8 +1314,9 @@ namespace Barotrauma.Steam itemsUpdated = false; foreach (var item in q.Items) { - if (item.Installed && CheckWorkshopItemEnabled(item) && !CheckWorkshopItemUpToDate(item)) + try { + if (!item.Installed || !CheckWorkshopItemEnabled(item) || CheckWorkshopItemUpToDate(item)) { continue; } if (!UpdateWorkshopItem(item, out string errorMsg)) { DebugConsole.ThrowError(errorMsg); @@ -1305,6 +1330,16 @@ namespace Barotrauma.Steam itemsUpdated = true; } } + catch (Exception e) + { + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })); + GameAnalyticsManager.AddErrorEventOnce( + "SteamManager.AutoUpdateWorkshopItems:" + e.Message, + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace); + } } }; @@ -1328,8 +1363,9 @@ namespace Barotrauma.Steam { errorMsg = ""; if (!item.Installed) { return false; } + bool wasSelected = CheckWorkshopItemSelected(item); if (!DisableWorkShopItem(item, out errorMsg)) { return false; } - if (!EnableWorkShopItem(item, allowFileOverwrite: false, errorMsg: out errorMsg)) { return false; } + if (!EnableWorkShopItem(item, allowFileOverwrite: false, errorMsg: out errorMsg, selectContentPackage: wasSelected)) { return false; } return true; } diff --git a/Barotrauma/BarotraumaClient/Source/Particles/DecalManager.cs b/Barotrauma/BarotraumaClient/Source/Particles/DecalManager.cs index 0bba7194d..a2be1cb1f 100644 --- a/Barotrauma/BarotraumaClient/Source/Particles/DecalManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Particles/DecalManager.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Xml.Linq; +using System.Linq; namespace Barotrauma.Particles { @@ -10,21 +11,47 @@ namespace Barotrauma.Particles public DecalManager() { - prefabs = new Dictionary(); + var decalElements = new Dictionary(); foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } - foreach (XElement element in doc.Root.Elements()) + bool allowOverriding = false; + var mainElement = doc.Root; + if (doc.Root.IsOverride()) { - if (prefabs.ContainsKey(element.Name.ToString())) - { - DebugConsole.ThrowError("Error in " + configFile + "! Each decal prefab must have a unique name."); - continue; - } - prefabs.Add(element.Name.ToString(), new DecalPrefab(element)); + 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 (decalElements.ContainsKey(name)) + { + if (allowOverriding || sourceElement.IsOverride()) + { + DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile}'", Color.Yellow); + decalElements.Remove(name); + } + else + { + DebugConsole.ThrowError($"Error in '{configFile}': Duplicate decal prefab '{name}' found in '{configFile}'! Each decal prefab must have a unique name. " + + "Use tags to override prefabs."); + continue; + } + + } + decalElements.Add(name, element); + } + } + //prefabs = decalElements.ToDictionary(d => d.Key, d => new DecalPrefab(d.Value)); + prefabs = new Dictionary(); + foreach (var kvp in decalElements) + { + prefabs.Add(kvp.Key, new DecalPrefab(kvp.Value)); } } diff --git a/Barotrauma/BarotraumaClient/Source/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/Source/Particles/ParticleManager.cs index 5a95a7351..168eef3a8 100644 --- a/Barotrauma/BarotraumaClient/Source/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Particles/ParticleManager.cs @@ -63,22 +63,48 @@ namespace Barotrauma.Particles public void LoadPrefabs() { - prefabs = new Dictionary(); + var particleElements = new Dictionary(); foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } - foreach (XElement element in doc.Root.Elements()) + bool allowOverriding = false; + var mainElement = doc.Root; + if (doc.Root.IsOverride()) { - if (prefabs.ContainsKey(element.Name.ToString())) - { - DebugConsole.ThrowError("Error in " + configFile + "! Each particle prefab must have a unique name."); - continue; - } - prefabs.Add(element.Name.ToString(), new ParticlePrefab(element)); + 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 (particleElements.ContainsKey(name)) + { + if (allowOverriding || sourceElement.IsOverride()) + { + DebugConsole.NewMessage($"Overriding the existing particle prefab '{name}' using the file '{configFile}'", Color.Yellow); + particleElements.Remove(name); + } + else + { + DebugConsole.ThrowError($"Error in '{configFile}': Duplicate particle prefab '{name}' found in '{configFile}'! Each particle prefab must have a unique name. " + + "Use tags to override prefabs."); + continue; + } + + } + particleElements.Add(name, element); + } + } + //prefabs = particleElements.ToDictionary(p => p.Key, p => new ParticlePrefab(p.Value)); + prefabs = new Dictionary(); + foreach (var kvp in particleElements) + { + prefabs.Add(kvp.Key, new ParticlePrefab(kvp.Value)); + } } public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null) diff --git a/Barotrauma/BarotraumaClient/Source/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/Source/Particles/ParticlePrefab.cs index c4d025776..eaa4d70ca 100644 --- a/Barotrauma/BarotraumaClient/Source/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/Source/Particles/ParticlePrefab.cs @@ -17,12 +17,12 @@ namespace Barotrauma.Particles private set; } - [Editable(0.0f, float.MaxValue, ToolTip = "How many seconds the particle remains alive."), Serialize(5.0f, false)] + [Editable(0.0f, float.MaxValue), Serialize(5.0f, false, description: "How many seconds the particle remains alive.")] public float LifeTime { get; private set; } - [Editable(ToolTip = "How long it takes for the particle to appear after spawning it."), Serialize(0.0f, false)] + [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMin { get; private set; } - [Editable(ToolTip = "How long it takes for the particle to appear after spawning it."), Serialize(0.0f, false)] + [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMax { get; private set; } //movement ----------------------------------------- @@ -57,7 +57,7 @@ namespace Barotrauma.Particles private float startRotationMin; public float StartRotationMinRad { get; private set; } - [Editable(ToolTip = "The minimum initial rotation of the particle (in degrees)."), Serialize(0.0f, false)] + [Editable, Serialize(0.0f, false, description: "The minimum initial rotation of the particle (in degrees).")] public float StartRotationMin { get { return startRotationMin; } @@ -71,7 +71,7 @@ namespace Barotrauma.Particles private float startRotationMax; public float StartRotationMaxRad { get; private set; } - [Editable(ToolTip = "The maximum initial rotation of the particle (in degrees)."), Serialize(0.0f, false)] + [Editable, Serialize(0.0f, false, description: "The maximum initial rotation of the particle (in degrees).")] public float StartRotationMax { get { return startRotationMax; } @@ -82,19 +82,19 @@ namespace Barotrauma.Particles } } - [Editable(ToolTip = "Should the particle face the direction it's moving towards."), Serialize(false, false)] + [Editable, Serialize(false, false, description: "Should the particle face the direction it's moving towards.")] public bool RotateToDirection { get; private set; } - [Editable(ToolTip = "Drag applied to the particle when it's moving through air."), Serialize(0.0f, false)] + [Editable, Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")] public float Drag { get; private set; } - [Editable(ToolTip = "Drag applied to the particle when it's moving through water."), Serialize(0.0f, false)] + [Editable, Serialize(0.0f, false, 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(ToolTip = "How much the velocity of the particle changes per second."), Serialize("0.0,0.0", false)] + [Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second.")] public Vector2 VelocityChange { get { return velocityChange; } @@ -108,7 +108,7 @@ namespace Barotrauma.Particles private Vector2 velocityChangeWater; public Vector2 VelocityChangeWaterDisplay { get; private set; } - [Editable(ToolTip = "How much the velocity of the particle changes per second when in water."), Serialize("0.0,0.0", false)] + [Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second when in water.")] public Vector2 VelocityChangeWater { get { return velocityChangeWater; } @@ -119,62 +119,62 @@ namespace Barotrauma.Particles } } - [Editable(0.0f, 10000.0f, ToolTip = "Drag applied to the particle when it's moving through water."), Serialize(0.0f, false)] + [Editable(0.0f, 10000.0f), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")] public float CollisionRadius { get; private set; } - [Editable(ToolTip = "Does the particle collide with the walls of the submarine and the level."), Serialize(false, false)] + [Editable, Serialize(false, false, description: "Does the particle collide with the walls of the submarine and the level.")] public bool UseCollision { get; private set; } - [Editable(ToolTip = "Does the particle disappear when it collides with something."), Serialize(false, false)] + [Editable, Serialize(false, false, description: "Does the particle disappear when it collides with something.")] public bool DeleteOnCollision { get; private set; } - [Editable(0.0f, 1.0f, ToolTip = "The friction coefficient of the particle, i.e. how much it slows down when it's sliding against a surface."), Serialize(0.5f, false)] + [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.")] public float Friction { get; private set; } - [Editable(0.0f, 1.0f, ToolTip = "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, false)] + [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).")] public float Restitution { get; private set; } //size ----------------------------------------- - [Editable(ToolTip = "The minimum initial size of the particle."), Serialize("1.0,1.0", false)] + [Editable, Serialize("1.0,1.0", false, description: "The minimum initial size of the particle.")] public Vector2 StartSizeMin { get; private set; } - [Editable(ToolTip = "The maximum initial size of the particle."), Serialize("1.0,1.0", false)] + [Editable, Serialize("1.0,1.0", false, description: "The maximum initial size of the particle.")] public Vector2 StartSizeMax { get; private set; } - [Editable(ToolTip = "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] - [Serialize("0.0,0.0", false)] + [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.")] public Vector2 SizeChangeMin { get; private set; } - [Editable(ToolTip = "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] - [Serialize("0.0,0.0", false)] + [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.")] public Vector2 SizeChangeMax { get; private set; } - [Editable(ToolTip = "How many seconds it takes for the particle to grow to it's initial size.")] - [Serialize(0.0f, false)] + [Editable] + [Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")] public float GrowTime { get; private set; } //rendering ----------------------------------------- - [Editable(ToolTip = "The initial color of the particle."), Serialize("1.0,1.0,1.0,1.0", false)] + [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] public Color StartColor { get; private set; } - [Editable(ToolTip = "The color of the particle at the end of its lifetime."), Serialize("1.0,1.0,1.0,1.0", false)] + [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The color of the particle at the end of its lifetime.")] public Color EndColor { get; private set; } - [Editable(ToolTip = "Should the particle be rendered in air, water or both."), Serialize(DrawTargetType.Air, false)] + [Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } - [Editable(ToolTip = "The type of blending to use when rendering the particle."), Serialize(ParticleBlendState.AlphaBlend, false)] + [Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } //animation ----------------------------------------- - [Editable(0.0f, float.MaxValue, ToolTip = "The duration of the particle's animation cycle (if it's animated)."), Serialize(1.0f, false)] + [Editable(0.0f, float.MaxValue), Serialize(1.0f, false, description: "The duration of the particle's animation cycle (if it's animated).")] public float AnimDuration { get; private set; } - [Editable(ToolTip = "Should the sprite animation be looped, or stay at the last frame when the animation finishes."), Serialize(true, false)] + [Editable, Serialize(true, false, description: "Should the sprite animation be looped, or stay at the last frame when the animation finishes.")] public bool LoopAnim { get; private set; } //---------------------------------------------------- diff --git a/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs index 30af3aa13..212ba27b8 100644 --- a/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs @@ -17,7 +17,7 @@ namespace Barotrauma get { return bodyShapeTexture; } } - public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color) + public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool mirror = false) { if (!Enabled) return; UpdateDrawPosition(); @@ -25,17 +25,23 @@ namespace Barotrauma new Vector3(DrawPosition, MathHelper.Clamp(deformSprite.Sprite.Depth, 0, 1)), deformSprite.Origin, -DrawRotation, - scale, - color, - flip: Dir < 0); + scale, color, Dir < 0, mirror); } - public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f) + public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false) { if (!Enabled) return; UpdateDrawPosition(); if (sprite == null) return; SpriteEffects spriteEffect = (Dir == 1.0f) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; + if (mirrorX) + { + spriteEffect = spriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; + } + if (mirrorY) + { + spriteEffect |= SpriteEffects.FlipVertically; + } sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), color, -drawRotation, scale, spriteEffect, depth); } @@ -134,14 +140,17 @@ namespace Barotrauma rot -= MathHelper.PiOver2; } - spriteBatch.Draw( - bodyShapeTexture, - new Vector2(DrawPosition.X, -DrawPosition.Y), - null, - color, - rot, - new Vector2(bodyShapeTexture.Width / 2, bodyShapeTexture.Height / 2), - 1.0f / bodyShapeTextureScale, SpriteEffects.None, 0.0f); + if (bodyShapeTexture != null) + { + spriteBatch.Draw( + bodyShapeTexture, + new Vector2(DrawPosition.X, -DrawPosition.Y), + null, + color, + rot, + new Vector2(bodyShapeTexture.Width / 2, bodyShapeTexture.Height / 2), + 1.0f / bodyShapeTextureScale, SpriteEffects.None, 0.0f); + } } public PosInfo ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime, string parentDebugName) diff --git a/Barotrauma/BarotraumaClient/Source/Program.cs b/Barotrauma/BarotraumaClient/Source/Program.cs index 506c0a219..a38405b09 100644 --- a/Barotrauma/BarotraumaClient/Source/Program.cs +++ b/Barotrauma/BarotraumaClient/Source/Program.cs @@ -9,8 +9,7 @@ using Barotrauma.Steam; using System.Diagnostics; #if WINDOWS -using System.Windows.Forms; -using Microsoft.Xna.Framework.Graphics; +using SharpDX; #endif #endregion @@ -31,135 +30,41 @@ namespace Barotrauma [STAThread] static void Main(string[] args) { - SteamManager.Initialize(); GameMain game = null; #if !DEBUG try { #endif + SteamManager.Initialize(); game = new GameMain(args); -#if !DEBUG - } - catch (Exception e) - { - if (game != null) game.Dispose(); - CrashDump(null, "crashreport.log", e); - return; - } -#endif - -#if DEBUG - game.Run(); -#else - bool attemptRestart = false; - - do - { - try - { - game.Run(); - attemptRestart = false; - } - catch (Exception e) - { - if (restartAttempts < 5 && CheckException(game, e)) - { - attemptRestart = true; - restartAttempts++; - } - else - { - CrashDump(game, "crashreport.log", e); - attemptRestart = false; - } - - } - } while (attemptRestart); -#endif - -#if !DEBUG - try - { -#endif + game.Run(); game.Dispose(); #if !DEBUG } catch (Exception e) { - CrashDump(null, "crashreport.log", e); - } -#endif - } - - private static bool CheckException(GameMain game, Exception e) - { -#if WINDOWS - - if (e is SharpDX.SharpDXException sharpDxException) - { - DebugConsole.NewMessage("SharpDX exception caught. (" - + e.Message + ", " + sharpDxException.ResultCode.Code.ToString("X") + "). Attempting to fix...", Microsoft.Xna.Framework.Color.Red); - - switch ((UInt32)sharpDxException.ResultCode.Code) + try { - case 0x887A0022: //DXGI_ERROR_NOT_CURRENTLY_AVAILABLE - switch (restartAttempts) - { - case 0: - //just wait and try again - DebugConsole.NewMessage("Retrying after 100 ms...", Microsoft.Xna.Framework.Color.Red); - System.Threading.Thread.Sleep(100); - return true; - case 1: - //force focus to this window - DebugConsole.NewMessage("Forcing focus to the window and retrying...", Microsoft.Xna.Framework.Color.Red); - var myForm = (Form)Control.FromHandle(game.Window.Handle); - myForm.Focus(); - return true; - case 2: - //try disabling hardware mode switch - if (GameMain.Config.WindowMode == WindowMode.Fullscreen) - { - DebugConsole.NewMessage("Failed to set fullscreen mode, switching configuration to borderless windowed.", Microsoft.Xna.Framework.Color.Red); - GameMain.Config.WindowMode = WindowMode.BorderlessWindowed; - GameMain.Config.SaveNewPlayerConfig(); - } - return false; - default: - DebugConsole.NewMessage("Failed to resolve the DXGI_ERROR_NOT_CURRENTLY_AVAILABLE exception. Give up and let it crash :(", Microsoft.Xna.Framework.Color.Red); - return false; - - } - case 0x80070057: //E_INVALIDARG/Invalid Arguments - DebugConsole.NewMessage("Invalid graphics settings, attempting to fix...", Microsoft.Xna.Framework.Color.Red); - - GameMain.Config.GraphicsWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width; - GameMain.Config.GraphicsHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height; - - DebugConsole.NewMessage("Display size set to " + GameMain.Config.GraphicsWidth + "x" + GameMain.Config.GraphicsHeight, Microsoft.Xna.Framework.Color.Red); - - game.ApplyGraphicsSettings(); - - return true; - default: - DebugConsole.NewMessage("Unknown SharpDX exception code (" + sharpDxException.ResultCode.Code.ToString("X") + ")", Microsoft.Xna.Framework.Color.Red); - return false; + CrashDump(game, "crashreport.log", e); } + catch (Exception e2) + { + CrashMessageBox("Barotrauma seems to have crashed, and failed to generate a crash report: " + + e2.Message + "\n" + e2.StackTrace.ToString(), + null); + } + game?.Dispose(); + return; } - #endif - - return false; } public static void CrashMessageBox(string message, string filePath) { -#if WINDOWS - MessageBox.Show(message, "Oops! Barotrauma just crashed.", MessageBoxButtons.OK, MessageBoxIcon.Error); -#endif + Microsoft.Xna.Framework.MessageBox.ShowWrapped(Microsoft.Xna.Framework.MessageBox.Flags.Error, "Oops! Barotrauma just crashed.", message); // Open the crash log. - Process.Start(filePath); + if (!string.IsNullOrWhiteSpace(filePath)) { Process.Start(filePath); } } static void CrashDump(GameMain game, string filePath, Exception exception) @@ -259,10 +164,18 @@ namespace Barotrauma sb.AppendLine("\n"); sb.AppendLine("Exception: " + exception.Message); +#if WINDOWS + if (exception is SharpDXException sharpDxException && ((uint)sharpDxException.HResult) == 0x887A0005) + { + var dxDevice = (SharpDX.Direct3D11.Device)game.GraphicsDevice.Handle; + sb.AppendLine("Device removed reason: " + dxDevice.DeviceRemovedReason.ToString()); + } +#endif if (exception.TargetSite != null) { sb.AppendLine("Target site: " + exception.TargetSite.ToString()); } + sb.AppendLine("Stack trace: "); sb.AppendLine(exception.StackTrace); sb.AppendLine("\n"); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs index 408f4d827..bc4b5ef3a 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs @@ -114,7 +114,7 @@ namespace Barotrauma } string savePath = SaveUtil.CreateSavePath(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer, saveNameBox.Text); - bool hasRequiredContentPackages = selectedSub.RequiredContentPackages.All(cp => GameMain.SelectedPackages.Any(cp2 => cp2.Name == cp)); + bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs index 55081b0a8..12db6795b 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs @@ -27,7 +27,7 @@ namespace Barotrauma private GUIComponent selectedLocationInfo; private GUIListBox selectedMissionInfo; - private GUIButton repairHullsButton, repairItemsButton; + private GUIButton repairHullsButton, replaceShuttlesButton, repairItemsButton; private GUIFrame characterPreviewFrame; @@ -281,6 +281,8 @@ namespace Barotrauma TextGetter = GetMoney }; + // repair hulls ----------------------------------------------- + var repairHullsHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), repairContent.RectTransform), childAnchor: Anchor.TopRight) { RelativeSpacing = 0.05f, @@ -295,7 +297,7 @@ namespace Barotrauma { ForceUpperCase = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), "500", textAlignment: Alignment.Right, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), CampaignMode.HullRepairCost.ToString(), textAlignment: Alignment.Right, font: GUI.LargeFont); repairHullsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("Repair"), style: "GUIButtonLarge") { OnClicked = (btn, userdata) => @@ -324,6 +326,8 @@ namespace Barotrauma CanBeFocused = false }; + // repair items ------------------------------------------- + var repairItemsHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), repairContent.RectTransform), childAnchor: Anchor.TopRight) { RelativeSpacing = 0.05f, @@ -338,7 +342,7 @@ namespace Barotrauma { ForceUpperCase = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), "500", textAlignment: Alignment.Right, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), CampaignMode.ItemRepairCost.ToString(), textAlignment: Alignment.Right, font: GUI.LargeFont); repairItemsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("Repair"), style: "GUIButtonLarge") { OnClicked = (btn, userdata) => @@ -367,6 +371,59 @@ namespace Barotrauma CanBeFocused = false }; + // replace lost shuttles ------------------------------------------- + + var replaceShuttlesHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), repairContent.RectTransform), childAnchor: Anchor.TopRight) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + new GUIImage(new RectTransform(new Vector2(0.3f, 1.0f), replaceShuttlesHolder.RectTransform, Anchor.CenterLeft), "ReplaceShuttlesButton") + { + IgnoreLayoutGroups = true, + CanBeFocused = false + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUI.LargeFont) + { + ForceUpperCase = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), CampaignMode.ShuttleReplaceCost.ToString(), textAlignment: Alignment.Right, font: GUI.LargeFont); + replaceShuttlesButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceShuttles"), style: "GUIButtonLarge") + { + OnClicked = (btn, userdata) => + { + if (GameMain.GameSession?.Submarine != null && + GameMain.GameSession.Submarine.LeftBehindSubDockingPortOccupied) + { + new GUIMessageBox("", TextManager.Get("ReplaceShuttleDockingPortOccupied")); + return true; + } + + if (campaign.PurchasedLostShuttles) + { + campaign.Money += CampaignMode.ShuttleReplaceCost; + campaign.PurchasedLostShuttles = false; + } + else + { + if (campaign.Money >= CampaignMode.ShuttleReplaceCost) + { + campaign.Money -= CampaignMode.ShuttleReplaceCost; + campaign.PurchasedLostShuttles = true; + } + } + GameMain.Client?.SendCampaignState(); + btn.GetChild().Selected = campaign.PurchasedLostShuttles; + + return true; + } + }; + new GUITickBox(new RectTransform(new Vector2(0.65f), replaceShuttlesButton.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(10, 0) }, "") + { + CanBeFocused = false + }; + + // mission info ------------------------------------------------------------------------- missionPanel = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.5f), container.RectTransform, Anchor.TopRight) @@ -853,6 +910,19 @@ namespace Barotrauma (Campaign.PurchasedItemRepairs || Campaign.Money >= CampaignMode.ItemRepairCost) && (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; + + if (GameMain.GameSession?.Submarine == null || !GameMain.GameSession.Submarine.SubsLeftBehind) + { + replaceShuttlesButton.Enabled = false; + replaceShuttlesButton.GetChild().Selected = false; + } + else + { + replaceShuttlesButton.Enabled = + (Campaign.PurchasedLostShuttles || Campaign.Money >= CampaignMode.ShuttleReplaceCost) && + (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); + replaceShuttlesButton.GetChild().Selected = Campaign.PurchasedLostShuttles; + } break; } } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/CharacterEditorScreen.cs similarity index 68% rename from Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs rename to Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/CharacterEditorScreen.cs index 35824c844..a30c09a00 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -9,11 +9,11 @@ using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics; -namespace Barotrauma +namespace Barotrauma.CharacterEditor { class CharacterEditorScreen : Screen { - private static CharacterEditorScreen instance; + public static CharacterEditorScreen Instance { get; private set; } private Camera cam; public override Camera Cam @@ -32,18 +32,23 @@ namespace Barotrauma } } - private bool ShowExtraRagdollControls => selectedLimbs.Any() && editLimbs || selectedJoints.Any() && editJoints; + private bool ShowExtraRagdollControls => editLimbs || editJoints; private Character character; private Vector2 spawnPosition; + + private bool editCharacterInfo; + private bool editRagdoll; private bool editAnimations; private bool editLimbs; private bool editJoints; private bool editIK; - private bool editRagdoll; + + private bool drawSkeleton; + private bool drawDamageModifiers; private bool showParamsEditor; private bool showSpritesheet; - private bool isFreezed; + private bool isFrozen; private bool autoFreeze; private bool limbPairEditing; private bool uniformScaling; @@ -55,28 +60,39 @@ namespace Barotrauma private bool showColliders; private bool displayWearables; private bool displayBackgroundColor; - private bool ragdollResetRequiresForceLoading; - private bool animationResetRequiresForceLoading; + private bool onlyShowSourceRectForSelectedLimbs; + private bool unrestrictSpritesheet; - private bool jointCreationMode; - private bool useMouseOffset; - private bool isExtrudingJoint; - private bool isDrawingJoint; - private Limb closestSelectedLimb; - private Limb targetLimb; + private enum JointCreationMode + { + None, + Select, + Create + } + + private JointCreationMode jointCreationMode; + private bool isDrawingLimb; + + private Rectangle newLimbRect; + private Limb jointStartLimb; + private Limb jointEndLimb; private Vector2? anchor1Pos; + private const float holdTime = 0.2f; + private double holdTimer; + private float spriteSheetZoom = 1; private float spriteSheetMinZoom = 0.25f; private float spriteSheetMaxZoom = 1; private int spriteSheetOffsetY = 20; - private int spriteSheetOffsetX; + private int spriteSheetOffsetX = 30; private bool hideBodySheet; private Color backgroundColor = new Color(0.2f, 0.2f, 0.2f, 1.0f); private Vector2 cameraOffset; private List selectedJoints = new List(); private List selectedLimbs = new List(); + private HashSet editedCharacters = new HashSet(); private bool isEndlessRunner; @@ -100,7 +116,6 @@ namespace Barotrauma GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f, 0); GUI.ForceMouseOn(null); - CalculateSpritesheetPosition(); if (Submarine.MainSub == null) { ResetVariables(); @@ -112,15 +127,22 @@ namespace Barotrauma isEndlessRunner = true; GameMain.LightManager.LightingEnabled = false; } - else if (instance == null) + else if (Instance == null) { ResetVariables(); } Submarine.MainSub.GodMode = true; if (Character.Controlled == null) { - SpawnCharacter(Character.HumanConfigFile); - //SpawnCharacter(AllFiles.First()); + var humanConfig = Character.HumanConfigFile; + if (string.IsNullOrEmpty(humanConfig)) + { + SpawnCharacter(AllFiles.First()); + } + else + { + SpawnCharacter(humanConfig); + } } else { @@ -130,7 +152,7 @@ namespace Barotrauma } OpenDoors(); GameMain.Instance.OnResolutionChanged += OnResolutionChanged; - instance = this; + Instance = this; if (!GameMain.Config.EditorDisclaimerShown) { @@ -140,18 +162,21 @@ namespace Barotrauma private void ResetVariables() { + editCharacterInfo = false; + editRagdoll = false; editAnimations = false; editLimbs = false; editJoints = false; editIK = false; - editRagdoll = false; + drawSkeleton = false; + drawDamageModifiers = false; showParamsEditor = false; showSpritesheet = false; - isFreezed = false; - autoFreeze = true; + isFrozen = false; + autoFreeze = false; limbPairEditing = true; uniformScaling = true; - lockSpriteOrigin = false; + lockSpriteOrigin = true; lockSpritePosition = false; lockSpriteSize = false; recalculateCollider = false; @@ -159,59 +184,79 @@ namespace Barotrauma showColliders = false; displayWearables = true; displayBackgroundColor = false; - ragdollResetRequiresForceLoading = false; - animationResetRequiresForceLoading = false; - jointCreationMode = false; - isExtrudingJoint = false; - isDrawingJoint = false; + jointCreationMode = JointCreationMode.None; + isDrawingLimb = false; + newLimbRect = Rectangle.Empty; cameraOffset = Vector2.Zero; - targetLimb = null; + jointEndLimb = null; anchor1Pos = null; - useMouseOffset = false; - closestSelectedLimb = null; + jointStartLimb = null; allFiles = null; + onlyShowSourceRectForSelectedLimbs = false; + unrestrictSpritesheet = false; + editedCharacters.Clear(); + selectedJoints.Clear(); + selectedLimbs.Clear(); + if (character != null) + { + if (character.AnimController != null) + { + if (character.AnimController.Collider != null) + { + character.AnimController.Collider.PhysEnabled = true; + } + } + } + character = null; Wizard.instance?.Reset(); } - private void Reset() + private void Reset(IEnumerable characters = null) { - ResetVariables(); - if (character != null) + if (characters == null) { - AnimParams.ForEach(a => a.Reset(true)); - RagdollParams.Reset(true); - RagdollParams.ClearHistory(); - CurrentAnimation.ClearHistory(); - if (!character.Removed) - { - character.Remove(); - } - character = null; + characters = editedCharacters; } + characters.ForEach(c => ResetParams(c)); + ResetVariables(); + } + + private void ResetParams(Character character) + { + character.Params.Reset(true); + foreach (var animation in character.AnimController.AllAnimParams) + { + animation.Reset(true); + animation.ClearHistory(); + } + character.AnimController.RagdollParams.Reset(true); + character.AnimController.RagdollParams.ClearHistory(); + character.ForceRun = false; + character.AnimController.ForceSelectAnimationType = AnimationType.NotDefined; } public override void Deselect() { base.Deselect(); - SoundPlayer.OverrideMusicType = null; GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume, 0); - GUI.ForceMouseOn(null); if (isEndlessRunner) { Submarine.MainSub.Remove(); + GameMain.World.ProcessChanges(); isEndlessRunner = false; Reset(); - GameMain.World.ProcessChanges(); + if (character != null && !character.Removed) + { + character.Remove(); + } } else { - if (character != null) - { - character.ForceRun = false; - character.AnimController.ForceSelectAnimationType = AnimationType.NotDefined; - } +#if !DEBUG + Reset(Character.CharacterList.Where(c => VanillaCharacters.Any(vchar => vchar == c.ConfigPath))); +#endif } GameMain.Instance.OnResolutionChanged -= OnResolutionChanged; GameMain.LightManager.LightingEnabled = true; @@ -222,10 +267,9 @@ namespace Barotrauma private void OnResolutionChanged() { CreateGUI(); - CalculateSpritesheetPosition(); } - private static string GetCharacterEditorTranslation(string tag) + public static string GetCharacterEditorTranslation(string tag) { return TextManager.Get(screenTextTag + tag); } @@ -233,11 +277,10 @@ namespace Barotrauma #region Main methods public override void AddToGUIUpdateList() { - //base.AddToGUIUpdateList(); - fileEditPanel.AddToGUIUpdateList(); modesPanel.AddToGUIUpdateList(); - toolsPanel.AddToGUIUpdateList(); + minorModesPanel.AddToGUIUpdateList(); + buttonsPanel.AddToGUIUpdateList(); optionsPanel.AddToGUIUpdateList(); characterSelectionPanel.AddToGUIUpdateList(); @@ -253,6 +296,19 @@ namespace Barotrauma if (showSpritesheet) { spriteSheetControls.AddToGUIUpdateList(); + Limb lastLimb = selectedLimbs.LastOrDefault(); + if (lastLimb == null) + { + var lastJoint = selectedJoints.LastOrDefault(); + if (lastJoint != null) + { + lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbB : lastJoint.LimbA; + } + } + if (lastLimb != null) + { + resetSpriteOrientationButtonParent.AddToGUIUpdateList(); + } } if (editRagdoll) { @@ -262,38 +318,79 @@ namespace Barotrauma { jointControls.AddToGUIUpdateList(); } - if (editLimbs) + if (editLimbs && !unrestrictSpritesheet) { limbControls.AddToGUIUpdateList(); } + if (ShowExtraRagdollControls) + { + createLimbButton.Enabled = editLimbs; + duplicateLimbButton.Enabled = selectedLimbs.Any(); + deleteSelectedButton.Enabled = selectedLimbs.Any() || selectedJoints.Any(); + createJointButton.Enabled = selectedLimbs.Any() || selectedJoints.Any(); + extraRagdollControls.AddToGUIUpdateList(); + if (createLimbButton.Enabled) + { + if (isDrawingLimb) + { + createLimbButton.Color = Color.Yellow; + createLimbButton.HoverColor = Color.Yellow; + } + else + { + createLimbButton.Color = Color.White; + createLimbButton.HoverColor = Color.White; + } + } + if (createJointButton.Enabled) + { + switch (jointCreationMode) + { + case JointCreationMode.Select: + case JointCreationMode.Create: + createJointButton.HoverColor = Color.Yellow; + createJointButton.Color = Color.Yellow; + break; + default: + createJointButton.HoverColor = Color.White; + createJointButton.Color = Color.White; + break; + } + } + } if (showParamsEditor) { ParamsEditor.Instance.EditorBox.Parent.AddToGUIUpdateList(); } - if (ShowExtraRagdollControls) - { - extraRagdollControls.AddToGUIUpdateList(); - } } public override void Update(double deltaTime) { base.Update(deltaTime); + if (Wizard.instance != null) { return; } spriteSheetRect = CalculateSpritesheetRectangle(); // Handle shortcut keys - if (GUI.KeyboardDispatcher.Subscriber == null && Wizard.instance == null) + if (GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(Keys.D1)) { - SetToggle(editLimbsToggle, true); + SetToggle(characterInfoToggle, !characterInfoToggle.Selected); } else if (PlayerInput.KeyHit(Keys.D2)) { - SetToggle(jointsToggle, true); + SetToggle(ragdollToggle, !ragdollToggle.Selected); } else if (PlayerInput.KeyHit(Keys.D3)) { - SetToggle(editAnimsToggle, true); + SetToggle(limbsToggle, !limbsToggle.Selected); + } + else if (PlayerInput.KeyHit(Keys.D4)) + { + SetToggle(jointsToggle, !jointsToggle.Selected); + } + else if (PlayerInput.KeyHit(Keys.D5)) + { + SetToggle(animsToggle, !animsToggle.Selected); } if (PlayerInput.KeyDown(Keys.LeftControl)) { @@ -309,7 +406,6 @@ namespace Barotrauma character.AnimController.ResetLimbs(); ClearWidgets(); CreateGUI(); - //ragdollResetRequiresForceLoading = true; ResetParamsEditor(); } if (editAnimations) @@ -317,8 +413,6 @@ namespace Barotrauma CurrentAnimation.Undo(); ClearWidgets(); ResetParamsEditor(); - //CreateGUI(); - animationResetRequiresForceLoading = true; } } else if (PlayerInput.KeyHit(Keys.R)) @@ -330,7 +424,6 @@ namespace Barotrauma character.AnimController.ResetLimbs(); ClearWidgets(); CreateGUI(); - //ragdollResetRequiresForceLoading = true; ResetParamsEditor(); } if (editAnimations) @@ -338,24 +431,57 @@ namespace Barotrauma CurrentAnimation.Redo(); ClearWidgets(); ResetParamsEditor(); - //CreateGUI(); - animationResetRequiresForceLoading = true; } } } else { Widget.EnableMultiSelect = false; + if (PlayerInput.KeyHit(Keys.C)) + { + SetToggle(showCollidersToggle, !showCollidersToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.Tab)) + { + SetToggle(paramsToggle, !paramsToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.L)) + { + SetToggle(lightsToggle, !lightsToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.M)) + { + SetToggle(damageModifiersToggle, !damageModifiersToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.N)) + { + SetToggle(skeletonToggle, !skeletonToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.T)) + { + SetToggle(spritesheetToggle, !spritesheetToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.I)) + { + SetToggle(ikToggle, !ikToggle.Selected); + } + if (PlayerInput.KeyHit(Keys.F5)) + { + RecreateRagdoll(); + } } - if (PlayerInput.KeyHit(Keys.C) && !PlayerInput.KeyDown(Keys.LeftControl)) + if (PlayerInput.KeyDown(InputType.Left) || PlayerInput.KeyDown(InputType.Right) || PlayerInput.KeyDown(InputType.Up) || PlayerInput.KeyDown(InputType.Down)) { - copyJointsToggle.Selected = !copyJointsToggle.Selected; + // Enable the main collider physics when the user is trying to move the character. + // It's possible that the physics are disabled, because the angle widgets handle input logic in the draw method (which they shouldn't) + character.AnimController.Collider.PhysEnabled = true; } if (character.IsHumanoid) { - if (PlayerInput.KeyHit(Keys.T) || PlayerInput.KeyHit(Keys.X)) + animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation; + if (animTestPoseToggle.Enabled && PlayerInput.KeyHit(Keys.X)) { - animTestPoseToggle.Selected = !animTestPoseToggle.Selected; + SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); } } if (PlayerInput.KeyHit(InputType.Run)) @@ -390,7 +516,7 @@ namespace Barotrauma { CurrentAnimation.ClearHistory(); animSelection.Select(index); - CurrentAnimation.CreateSnapshot(); + CurrentAnimation.StoreSnapshot(); } } if (!PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.E)) @@ -431,85 +557,107 @@ namespace Barotrauma { ResetParamsEditor(); } - jointCreationMode = false; - closestSelectedLimb = null; + jointCreationMode = JointCreationMode.None; + isDrawingLimb = false; } if (PlayerInput.KeyHit(Keys.Delete)) { DeleteSelected(); } - if (editLimbs && PlayerInput.KeyDown(Keys.LeftControl)) - { - var selectedLimb = selectedLimbs.FirstOrDefault(); - if (selectedLimb != null) - { - if (PlayerInput.KeyHit(Keys.C)) - { - CopyLimb(selectedLimb); - } - } - } if (ShowExtraRagdollControls && PlayerInput.KeyDown(Keys.LeftControl)) { if (PlayerInput.KeyHit(Keys.E)) { - jointCreationMode = !jointCreationMode; - useMouseOffset = true; + ToggleJointCreationMode(); } } - if (jointCreationMode) - { - createJointButton.HoverColor = Color.LightGreen; - createJointButton.Color = Color.LightGreen; - } - else - { - createJointButton.HoverColor = Color.White; - createJointButton.Color = Color.White; - } UpdateJointCreation(); + UpdateLimbCreation(); if (PlayerInput.KeyHit(Keys.Left)) { - foreach (var limb in selectedLimbs) - { - var newRect = limb.ActiveSprite.SourceRect; - newRect.X--; - UpdateSourceRect(limb, newRect); - } + Nudge(Keys.Left); } if (PlayerInput.KeyHit(Keys.Right)) { - foreach (var limb in selectedLimbs) - { - var newRect = limb.ActiveSprite.SourceRect; - newRect.X++; - UpdateSourceRect(limb, newRect); - } + Nudge(Keys.Right); } if (PlayerInput.KeyHit(Keys.Down)) { - foreach (var limb in selectedLimbs) - { - var newRect = limb.ActiveSprite.SourceRect; - newRect.Y++; - UpdateSourceRect(limb, newRect); - } + Nudge(Keys.Down); } if (PlayerInput.KeyHit(Keys.Up)) { - foreach (var limb in selectedLimbs) + Nudge(Keys.Up); + } + if (PlayerInput.KeyDown(Keys.Left)) + { + holdTimer += deltaTime; + if (holdTimer > holdTime) { - var newRect = limb.ActiveSprite.SourceRect; - newRect.Y--; - UpdateSourceRect(limb, newRect); + Nudge(Keys.Left); } } + else if (PlayerInput.KeyDown(Keys.Right)) + { + holdTimer += deltaTime; + if (holdTimer > holdTime) + { + Nudge(Keys.Right); + } + } + else if (PlayerInput.KeyDown(Keys.Down)) + { + holdTimer += deltaTime; + if (holdTimer > holdTime) + { + Nudge(Keys.Down); + } + } + else if (PlayerInput.KeyDown(Keys.Up)) + { + holdTimer += deltaTime; + if (holdTimer > holdTime) + { + Nudge(Keys.Up); + } + } + else + { + holdTimer = 0; + } + if (isFrozen) + { + float moveSpeed = (float)deltaTime * 300.0f / Cam.Zoom; + if (PlayerInput.KeyDown(Keys.LeftShift)) + { + moveSpeed *= 4; + } + if (PlayerInput.KeyDown(Keys.W)) + { + cameraOffset.Y += moveSpeed; + } + if (PlayerInput.KeyDown(Keys.A)) + { + cameraOffset.X -= moveSpeed; + } + if (PlayerInput.KeyDown(Keys.S)) + { + cameraOffset.Y -= moveSpeed; + } + if (PlayerInput.KeyDown(Keys.D)) + { + cameraOffset.X += moveSpeed; + } + Vector2 max = new Vector2(GameMain.GraphicsWidth * 0.3f, GameMain.GraphicsHeight * 0.38f) / Cam.Zoom; + Vector2 min = -max; + cameraOffset = Vector2.Clamp(cameraOffset, min, max); + } } - if (!isFreezed && Wizard.instance == null) + if (!isFrozen) { if (character.AnimController.Invalid) { - Reset(); + Reset(new Character[] { character }); SpawnCharacter(currentCharacterConfig); } @@ -588,8 +736,9 @@ namespace Barotrauma optionsToggle?.UpdateOpenState((float)deltaTime, new Vector2(optionsPanel.Rect.Width + rightArea.RectTransform.AbsoluteOffset.X, 0), optionsPanel.RectTransform); fileEditToggle?.UpdateOpenState((float)deltaTime, new Vector2(-fileEditPanel.Rect.Width - rightArea.RectTransform.AbsoluteOffset.X, 0), fileEditPanel.RectTransform); characterPanelToggle?.UpdateOpenState((float)deltaTime, new Vector2(-characterSelectionPanel.Rect.Width - rightArea.RectTransform.AbsoluteOffset.X, 0), characterSelectionPanel.RectTransform); + minorModesToggle?.UpdateOpenState((float)deltaTime, new Vector2(-minorModesPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), minorModesPanel.RectTransform); modesToggle?.UpdateOpenState((float)deltaTime, new Vector2(-modesPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), modesPanel.RectTransform); - toolsToggle?.UpdateOpenState((float)deltaTime, new Vector2(-toolsPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), toolsPanel.RectTransform); + buttonsPanelToggle?.UpdateOpenState((float)deltaTime, new Vector2(-buttonsPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), buttonsPanel.RectTransform); } /// @@ -598,7 +747,7 @@ namespace Barotrauma private Vector2 scaledMouseSpeed; public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - if (isFreezed) + if (isFrozen) { Timing.Alpha = 0.0f; } @@ -647,6 +796,16 @@ namespace Barotrauma // GUI spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + if (drawDamageModifiers) + { + foreach (Limb limb in character.AnimController.Limbs) + { + if (selectedLimbs.Contains(limb) || selectedLimbs.None()) + { + limb.DrawDamageModifiers(spriteBatch, cam, SimToScreen(limb.SimPosition), isScreenSpace: true); + } + } + } if (editAnimations) { DrawAnimationControls(spriteBatch, (float)deltaTime); @@ -655,7 +814,7 @@ namespace Barotrauma { DrawLimbEditor(spriteBatch); } - if (editRagdoll || editJoints || editLimbs) + if (drawSkeleton || editRagdoll || editJoints || editLimbs || editIK) { DrawRagdoll(spriteBatch, (float)deltaTime); } @@ -663,54 +822,61 @@ namespace Barotrauma { DrawSpritesheetEditor(spriteBatch, (float)deltaTime); } - if (jointCreationMode) + if (isDrawingLimb) { - var textPos = new Vector2(GameMain.GraphicsWidth / 2 - 120, GameMain.GraphicsHeight / 4); - if (isExtrudingJoint) + if (spriteSheetRect.Contains(PlayerInput.MousePosition)) { - var selectedJoint = selectedJoints.LastOrDefault(); - if (selectedJoint != null) + GUI.DrawRectangle(spriteBatch, newLimbRect, Color.Yellow); + } + } + if (jointCreationMode != JointCreationMode.None) + { + 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); + } + else + { + GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectLimbToConnect"), Color.Yellow, font: GUI.LargeFont); + } + if (jointStartLimb != null && jointStartLimb.ActiveSprite != null) + { + GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointStartLimb), Color.Yellow, thickness: 3); + GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointStartLimb), Color.Yellow, thickness: 3); + } + if (jointEndLimb != null && jointEndLimb.ActiveSprite != null) + { + GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointEndLimb), Color.LightGreen, thickness: 3); + GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointEndLimb), Color.LightGreen, thickness: 3); + } + if (spriteSheetRect.Contains(PlayerInput.MousePosition)) + { + if (jointStartLimb != null) { - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("CreatingNewJoint"), Color.White, font: GUI.LargeFont); - if (spriteSheetRect.Contains(PlayerInput.MousePosition)) - { - var startPos = GetLimbSpritesheetRect(selectedJoint.LimbB).Center.ToVector2(); - var offset = ConvertUnits.ToDisplayUnits(selectedJoint.LocalAnchorB) * spriteSheetZoom; - offset.Y = -offset.Y; - DrawJointCreationOnSpritesheet(spriteBatch, startPos + offset); - } - else - { - DrawJointCreationOnRagdoll(spriteBatch, SimToScreen(selectedJoint.WorldAnchorB)); - } + var startPos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2(); + var offset = anchor1Pos ?? Vector2.Zero; + offset = -offset; + startPos += offset; + GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, Color.LightGreen, width: 3); } } - else if (isDrawingJoint) + else { - if (closestSelectedLimb != null) + if (jointStartLimb != null) { - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("CreatingNewJoint"), Color.White, font: GUI.LargeFont); - if (spriteSheetRect.Contains(PlayerInput.MousePosition)) - { - var startPos = GetLimbSpritesheetRect(closestSelectedLimb).Center.ToVector2(); - if (anchor1Pos.HasValue) - { - var offset = anchor1Pos.Value; - offset = -offset; - startPos += offset; - } - DrawJointCreationOnSpritesheet(spriteBatch, startPos); - } - else - { - var startPos = anchor1Pos.HasValue - ? SimToScreen(closestSelectedLimb.SimPosition + Vector2.Transform(ConvertUnits.ToSimUnits(anchor1Pos.Value), Matrix.CreateRotationZ(closestSelectedLimb.Rotation))) - : SimToScreen(closestSelectedLimb.SimPosition); - DrawJointCreationOnRagdoll(spriteBatch, startPos); - } + // 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, Color.LightGreen, 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); + } if (isEndlessRunner) { Structure wall = CurrentWall.walls.FirstOrDefault(); @@ -718,22 +884,58 @@ namespace Barotrauma GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUI.SubmarineIcon, Color.White); } GUI.Draw(Cam, spriteBatch); - if (isFreezed) + if (isFrozen) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 35, 200), GetCharacterEditorTranslation("Frozen"), Color.Blue, Color.White * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 40, 200), GetCharacterEditorTranslation("Frozen"), Color.Blue, Color.White * 0.5f, 10, GUI.LargeFont); } if (animTestPoseToggle.Selected) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 300), GetCharacterEditorTranslation("AnimationTestPoseEnabled"), Color.Blue, Color.White * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 300), GetCharacterEditorTranslation("AnimationTestPoseEnabled"), Color.White, Color.Black * 0.5f, 10, GUI.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); + } + if (selectedLimbs.Count == 1) + { + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedLimbs.First().Params.Name}", Color.White, font: GUI.LargeFont); } if (showSpritesheet) { - var topLeft = spriteSheetControls.RectTransform.TopLeft; - GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 300, GameMain.GraphicsHeight - 80), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUI.Font); - DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 510, GameMain.GraphicsHeight - 60), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, - angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false); + Limb lastLimb = selectedLimbs.LastOrDefault(); + if (lastLimb == null) + { + var lastJoint = selectedJoints.LastOrDefault(); + if (lastJoint != null) + { + lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbB : lastJoint.LimbA; + } + } + if (lastLimb != null) + { + 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); + float orientation = useSpritesheetOrientation ? RagdollParams.SpritesheetOrientation : lastLimb.Params.SpriteOrientation; + DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 560 * 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)); + if (limbPairEditing) + { + UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation", angle)); + } + }, circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false); + } + 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); + DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 560 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, + angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false); + } } - // Debug if (GameMain.DebugDraw) { @@ -768,142 +970,205 @@ namespace Barotrauma #region Ragdoll Manipulation private void UpdateJointCreation() { - isExtrudingJoint = !editLimbs && editJoints && jointCreationMode; - isDrawingJoint = !editJoints && editLimbs && jointCreationMode; - if (isExtrudingJoint) + if (jointCreationMode == JointCreationMode.None) + { + jointStartLimb = null; + jointEndLimb = null; + anchor1Pos = null; + return; + } + if (editJoints) { var selectedJoint = selectedJoints.LastOrDefault(); if (selectedJoint != null) { - if (spriteSheetRect.Contains(PlayerInput.MousePosition)) + if (jointCreationMode == JointCreationMode.Create) { - targetLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != selectedJoint.LimbB && l.ActiveSprite != null); - if (targetLimb != null && PlayerInput.LeftButtonClicked()) + if (spriteSheetRect.Contains(PlayerInput.MousePosition)) { - Vector2 anchor1 = ConvertUnits.ToDisplayUnits(selectedJoint.LocalAnchorB); - Vector2 anchor2 = (GetLimbSpritesheetRect(targetLimb).Center.ToVector2() - PlayerInput.MousePosition) / spriteSheetZoom; - anchor2.X = -anchor2.X; - ExtrudeJoint(selectedJoint, targetLimb.limbParams.ID, anchor1, anchor2); + jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + if (jointEndLimb != null && PlayerInput.LeftButtonClicked()) + { + Vector2 anchor1 = anchor1Pos.HasValue ? anchor1Pos.Value / spriteSheetZoom : Vector2.Zero; + anchor1.X = -anchor1.X; + Vector2 anchor2 = (GetLimbSpritesheetRect(jointEndLimb).Center.ToVector2() - PlayerInput.MousePosition) / spriteSheetZoom; + anchor2.X = -anchor2.X; + CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1, anchor2); + jointCreationMode = JointCreationMode.None; + } + } + else + { + jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + if (jointEndLimb != null && PlayerInput.LeftButtonClicked()) + { + Vector2 anchor2 = ConvertUnits.ToDisplayUnits(jointEndLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); + CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1Pos, anchor2); + jointCreationMode = JointCreationMode.None; + } } } else { - targetLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != selectedJoint.LimbB && l.ActiveSprite != null); - if (targetLimb != null && PlayerInput.LeftButtonClicked()) + jointStartLimb = selectedJoint.LimbB; + if (spriteSheetRect.Contains(PlayerInput.MousePosition)) { - Vector2 anchor1 = ConvertUnits.ToDisplayUnits(selectedJoint.LocalAnchorB); - Vector2 anchor2 = ConvertUnits.ToDisplayUnits(targetLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); - ExtrudeJoint(selectedJoint, targetLimb.limbParams.ID, anchor1, anchor2); + anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition; + } + else + { + anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); + } + if (PlayerInput.LeftButtonClicked()) + { + jointCreationMode = JointCreationMode.Create; } } } else { - targetLimb = null; + jointCreationMode = JointCreationMode.None; } } - else if (isDrawingJoint) + else if (editLimbs) { if (selectedLimbs.Any()) { if (spriteSheetRect.Contains(PlayerInput.MousePosition)) { - if (closestSelectedLimb == null) + if (jointCreationMode == JointCreationMode.Create) { - closestSelectedLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + if (jointEndLimb != null && PlayerInput.LeftButtonClicked()) + { + Vector2 anchor1 = anchor1Pos.HasValue ? anchor1Pos.Value / spriteSheetZoom : Vector2.Zero; + anchor1.X = -anchor1.X; + Vector2 anchor2 = (GetLimbSpritesheetRect(jointEndLimb).Center.ToVector2() - PlayerInput.MousePosition) / spriteSheetZoom; + anchor2.X = -anchor2.X; + CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1, anchor2); + jointCreationMode = JointCreationMode.None; + } } - if (anchor1Pos == null && useMouseOffset) + else if (PlayerInput.LeftButtonClicked()) { - anchor1Pos = GetLimbSpritesheetRect(closestSelectedLimb).Center.ToVector2() - PlayerInput.MousePosition; - } - targetLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != closestSelectedLimb && l.ActiveSprite != null); - if (targetLimb != null && PlayerInput.LeftButtonClicked()) - { - Vector2 anchor1 = anchor1Pos.HasValue ? anchor1Pos.Value / spriteSheetZoom : Vector2.Zero; - anchor1.X = -anchor1.X; - Vector2 anchor2 = (GetLimbSpritesheetRect(targetLimb).Center.ToVector2() - PlayerInput.MousePosition) / spriteSheetZoom; - anchor2.X = -anchor2.X; - CreateJoint(closestSelectedLimb.limbParams.ID, targetLimb.limbParams.ID, anchor1, anchor2); - jointCreationMode = false; - closestSelectedLimb = null; + jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition; + jointCreationMode = JointCreationMode.Create; } } else { - if (closestSelectedLimb == null) + if (jointCreationMode == JointCreationMode.Create) { - closestSelectedLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + if (jointEndLimb != null && PlayerInput.LeftButtonClicked()) + { + Vector2 anchor1 = anchor1Pos ?? Vector2.Zero; + Vector2 anchor2 = ConvertUnits.ToDisplayUnits(jointEndLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); + CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1, anchor2); + jointCreationMode = JointCreationMode.None; + } } - if (anchor1Pos == null && useMouseOffset) + else if (PlayerInput.LeftButtonClicked()) { - anchor1Pos = ConvertUnits.ToDisplayUnits(closestSelectedLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); - } - targetLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != closestSelectedLimb && l.ActiveSprite != null); - if (targetLimb != null && PlayerInput.LeftButtonClicked()) - { - Vector2 anchor1 = anchor1Pos ?? Vector2.Zero; - Vector2 anchor2 = ConvertUnits.ToDisplayUnits(targetLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); - CreateJoint(closestSelectedLimb.limbParams.ID, targetLimb.limbParams.ID, anchor1, anchor2); - jointCreationMode = false; - closestSelectedLimb = null; + jointStartLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); + jointCreationMode = JointCreationMode.Create; } } } else { - targetLimb = null; - anchor1Pos = null; + jointCreationMode = JointCreationMode.None; + } + } + } + + private void UpdateLimbCreation() + { + if (!isDrawingLimb) + { + newLimbRect = Rectangle.Empty; + return; + } + if (!editLimbs) + { + SetToggle(limbsToggle, true); + } + if (spriteSheetRect.Contains(PlayerInput.MousePosition)) + { + if (PlayerInput.LeftButtonHeld()) + { + if (newLimbRect == Rectangle.Empty) + { + newLimbRect = new Rectangle((int)PlayerInput.MousePosition.X, (int)PlayerInput.MousePosition.Y, 0, 0); + } + else + { + newLimbRect.Size = new Point((int)PlayerInput.MousePosition.X - newLimbRect.X, (int)PlayerInput.MousePosition.Y - newLimbRect.Y); + } + newLimbRect.Size = new Point(Math.Max(newLimbRect.Width, 2), Math.Max(newLimbRect.Height, 2)); + } + if (PlayerInput.LeftButtonClicked()) + { + // Take the offset and the zoom into account + newLimbRect.Location = new Point(newLimbRect.X - spriteSheetOffsetX, newLimbRect.Y - spriteSheetOffsetY); + newLimbRect = newLimbRect.Divide(spriteSheetZoom); + CreateNewLimb(newLimbRect); + isDrawingLimb = false; + newLimbRect = Rectangle.Empty; } } else { - targetLimb = null; - anchor1Pos = null; + newLimbRect = Rectangle.Empty; } } private void CopyLimb(Limb limb) { if (limb == null) { return; } - //RagdollParams.StoreState(); - // TODO: copy all params and sub params -> use a generic method? + // TODO: copy all params and sub params -> use a generic method/reflection? var rect = limb.ActiveSprite.SourceRect; - var spriteParams = limb.limbParams.normalSpriteParams; - if (spriteParams == null) - { - spriteParams = limb.limbParams.deformSpriteParams; - } + var spriteParams = limb.Params.GetSprite(); var newLimbElement = new XElement("limb", new XAttribute("id", RagdollParams.Limbs.Last().ID + 1), - new XAttribute("radius", limb.limbParams.Radius), - new XAttribute("width", limb.limbParams.Width), - new XAttribute("height", limb.limbParams.Height), - new XAttribute("mass", limb.limbParams.Mass), + new XAttribute("radius", limb.Params.Radius), + new XAttribute("width", limb.Params.Width), + 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}"))); + CreateLimb(newLimbElement); + } + + private void CreateNewLimb(Rectangle sourceRect) + { + var newLimbElement = new XElement("limb", + new XAttribute("id", RagdollParams.Limbs.Last().ID + 1), + new XAttribute("width", sourceRect.Width * RagdollParams.TextureScale), + 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}"))); + CreateLimb(newLimbElement); + lockSpriteOriginToggle.Selected = false; + recalculateColliderToggle.Selected = true; + } + + private void CreateLimb(XElement newElement) + { var lastLimbElement = RagdollParams.MainElement.Elements("limb").Last(); - lastLimbElement.AddAfterSelf(newLimbElement); - var newLimbParams = new LimbParams(newLimbElement, RagdollParams); + lastLimbElement.AddAfterSelf(newElement); + var newLimbParams = new RagdollParams.LimbParams(newElement, RagdollParams); RagdollParams.Limbs.Add(newLimbParams); character.AnimController.Recreate(); CreateTextures(); TeleportTo(spawnPosition); ClearWidgets(); ClearSelection(); - selectedLimbs.Add(character.AnimController.Limbs.Single(l => l.limbParams == newLimbParams)); + selectedLimbs.Add(character.AnimController.Limbs.Single(l => l.Params == newLimbParams)); ResetParamsEditor(); - ragdollResetRequiresForceLoading = true; - } - - /// - /// Creates a new joint between the last limb of the given joint and the target limb. - /// - private void ExtrudeJoint(LimbJoint joint, int targetLimb, Vector2? anchor1 = null, Vector2? anchor2 = null) - { - if (joint == null) { return; } - CreateJoint(joint.jointParams.Limb2, targetLimb, anchor1, anchor2); } /// @@ -911,6 +1176,11 @@ namespace Barotrauma /// private void CreateJoint(int fromLimb, int toLimb, Vector2? anchor1 = null, Vector2? anchor2 = null) { + if (RagdollParams.Joints.Any(j => j.Limb1 == fromLimb && j.Limb2 == toLimb)) + { + DebugConsole.ThrowError(GetCharacterEditorTranslation("ExistingJointFound").Replace("[limbid1]", fromLimb.ToString()).Replace("[limbid2]", toLimb.ToString())); + return; + } //RagdollParams.StoreState(); Vector2 a1 = anchor1 ?? Vector2.Zero; Vector2 a2 = anchor2 ?? Vector2.Zero; @@ -932,17 +1202,15 @@ namespace Barotrauma return; } lastJointElement.AddAfterSelf(newJointElement); - var newJointParams = new JointParams(newJointElement, RagdollParams); + var newJointParams = new RagdollParams.JointParams(newJointElement, RagdollParams); RagdollParams.Joints.Add(newJointParams); character.AnimController.Recreate(); CreateTextures(); TeleportTo(spawnPosition); ClearWidgets(); ClearSelection(); - selectedJoints.Add(character.AnimController.LimbJoints.Single(j => j.jointParams == newJointParams)); - jointsToggle.Selected = true; - ResetParamsEditor(); - ragdollResetRequiresForceLoading = true; + SetToggle(jointsToggle, true); + selectedJoints.Add(character.AnimController.LimbJoints.Single(j => j.Params == newJointParams)); } /// @@ -954,8 +1222,8 @@ namespace Barotrauma for (int i = 0; i < selectedJoints.Count; i++) { var joint = selectedJoints[i]; - joint.jointParams.Element.Remove(); - RagdollParams.Joints.Remove(joint.jointParams); + joint.Params.Element.Remove(); + RagdollParams.Joints.Remove(joint.Params); } var removedIDs = new List(); for (int i = 0; i < selectedLimbs.Count; i++) @@ -971,9 +1239,9 @@ namespace Barotrauma DebugConsole.ThrowError("Can't remove the main limb, because it will cause unreveratable issues."); continue; } - removedIDs.Add(limb.limbParams.ID); - limb.limbParams.Element.Remove(); - RagdollParams.Limbs.Remove(limb.limbParams); + removedIDs.Add(limb.Params.ID); + limb.Params.Element.Remove(); + RagdollParams.Limbs.Remove(limb.Params); } // Recreate ids var renamedIDs = new Dictionary(); @@ -990,7 +1258,7 @@ namespace Barotrauma } } // Refresh/recreate joints - var jointsToRemove = new List(); + var jointsToRemove = new List(); for (int i = 0; i < RagdollParams.Joints.Count; i++) { var joint = RagdollParams.Joints[i]; @@ -1025,7 +1293,6 @@ namespace Barotrauma RagdollParams.Joints.Remove(jointParam); } RecreateRagdoll(); - ragdollResetRequiresForceLoading = true; } #endregion @@ -1146,7 +1413,7 @@ namespace Barotrauma { if (allFiles == null) { - allFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).OrderBy(f => f).ToList(); + allFiles = Character.ConfigFilePaths.OrderBy(p => p).ToList(); allFiles.ForEach(f => DebugConsole.NewMessage(f, Color.White)); } return allFiles; @@ -1184,7 +1451,7 @@ namespace Barotrauma private void GetCurrentCharacterIndex() { - characterIndex = AllFiles.IndexOf(Character.GetConfigFile(character.SpeciesName)); + characterIndex = AllFiles.IndexOf(Character.GetConfigFilePath(character.SpeciesName)); } private void IncreaseIndex() @@ -1223,7 +1490,7 @@ namespace Barotrauma } if (configFile == Character.HumanConfigFile && selectedJob != null) { - var characterInfo = new CharacterInfo(configFile, jobPrefab: JobPrefab.List.First(job => job.Identifier == selectedJob)); + var characterInfo = new CharacterInfo(configFile, jobPrefab: JobPrefab.Get(selectedJob)); character = Character.Create(configFile, spawnPosition, ToolBox.RandomSeed(8), characterInfo, hasAi: false, ragdoll: ragdoll); character.GiveJobItems(); HideWearables(); @@ -1271,8 +1538,6 @@ namespace Barotrauma wayPoint = WayPoint.GetRandom(sub: Submarine.MainSub); } spawnPosition = wayPoint.WorldPosition; - ragdollResetRequiresForceLoading = false; - animationResetRequiresForceLoading = false; } private void OnPostSpawn() @@ -1290,9 +1555,10 @@ namespace Barotrauma ClearWidgets(); ClearSelection(); ResetParamsEditor(); - CurrentAnimation.CreateSnapshot(); - RagdollParams.CreateSnapshot(); + CurrentAnimation.StoreSnapshot(); + RagdollParams.StoreSnapshot(); Cam.Position = character.WorldPosition; + editedCharacters.Add(character); } private void ClearWidgets() @@ -1307,28 +1573,34 @@ namespace Barotrauma { selectedLimbs.Clear(); selectedJoints.Clear(); + foreach (var w in jointSelectionWidgets.Values) + { + w.refresh(); + w.linkedWidget?.refresh(); + } } private void RecreateRagdoll(RagdollParams ragdoll = null) { + RagdollParams.Apply(); character.AnimController.Recreate(ragdoll); TeleportTo(spawnPosition); // For some reason Enumerable.Contains() method does not find the match, threfore the conversion to a list. - var selectedJointParams = selectedJoints.Select(j => j.jointParams).ToList(); - var selectedLimbParams = selectedLimbs.Select(l => l.limbParams).ToList(); + var selectedJointParams = selectedJoints.Select(j => j.Params).ToList(); + var selectedLimbParams = selectedLimbs.Select(l => l.Params).ToList(); CreateTextures(); ClearWidgets(); ClearSelection(); foreach (var joint in character.AnimController.LimbJoints) { - if (selectedJointParams.Contains(joint.jointParams)) + if (selectedJointParams.Contains(joint.Params)) { selectedJoints.Add(joint); } } foreach (var limb in character.AnimController.Limbs) { - if (selectedLimbParams.Contains(limb.limbParams)) + if (selectedLimbParams.Contains(limb.Params)) { selectedLimbs.Add(limb); } @@ -1349,7 +1621,7 @@ namespace Barotrauma Cam.Position = character.WorldPosition; } - private bool CreateCharacter(string name, string mainFolder, bool isHumanoid, ContentPackage contentPackage = null, params object[] ragdollConfig) + public bool CreateCharacter(string name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable animations = null) { var vanilla = GameMain.VanillaContent; @@ -1362,35 +1634,11 @@ namespace Barotrauma #endif } if (contentPackage == null) - { - string modName = "NewCharacterMod"; - if (ContentPackage.List.Any(cp => cp.Name == modName)) - { - string tempName = modName; - for (int i = 0; i < 100; i++) - { - tempName = modName + i.ToString(); - if (ContentPackage.List.None(cp => cp.Name == tempName)) - { - modName = tempName; - break; - } - } - } - contentPackage = ContentPackage.CreatePackage(modName, Path.Combine(ContentPackage.Folder, $"{modName}.xml"), false); - ContentPackage.List.Add(contentPackage); - } - if (contentPackage == null) { // This should not be possible. DebugConsole.ThrowError(GetCharacterEditorTranslation("NoContentPackageSelected")); return false; } - if (!GameMain.Config.SelectedContentPackages.Contains(contentPackage)) - { - GameMain.Config.SelectedContentPackages.Add(contentPackage); - GameMain.Config.SaveNewPlayerConfig(); - } #if !DEBUG if (vanilla != null && contentPackage == vanilla) { @@ -1398,26 +1646,79 @@ namespace Barotrauma return false; } #endif - string speciesName = name; - // Config file - string configFilePath = Path.Combine(mainFolder, $"{speciesName}.xml").Replace(@"\", @"/"); - if (ContentPackage.GetFilesOfType(GameMain.SelectedPackages, ContentType.Character).Any(path => path.Contains(speciesName))) + // Content package + if (!GameMain.Config.SelectedContentPackages.Contains(contentPackage)) { - GUI.AddMessage(GetCharacterEditorTranslation("ExistingCharacterFound"), Color.Red, font: GUI.LargeFont); - // TODO: add a prompt: "Do you want to replace it?" + functionality - return false; + GameMain.Config.SelectContentPackage(contentPackage); + } + GameMain.Config.SaveNewPlayerConfig(); + + // Config file + string configFilePath = Path.Combine(mainFolder, $"{name}.xml").Replace(@"\", @"/"); + var duplicate = Character.ConfigFiles.FirstOrDefault(f => (f.Root.IsOverride() ? f.Root.FirstElement() : f.Root).GetAttributeString("speciesname", string.Empty).Equals(name, StringComparison.OrdinalIgnoreCase)); + XElement overrideElement = null; + if (duplicate != null) + { + allFiles = null; + if (!File.Exists(configFilePath)) + { + // If the file exists, we just want to overwrite it. + // If the file does not exist, it's part of a different content package -> we'll want to override it. + overrideElement = new XElement("override"); + } } - // Create the config file - XElement mainElement = new XElement("Character", - new XAttribute("name", speciesName), - new XAttribute("humanoid", isHumanoid), - new XElement("ragdolls", new XAttribute("folder", Path.Combine(mainFolder, $"Ragdolls/").Replace(@"\", @"/"))), - new XElement("animations", new XAttribute("folder", Path.Combine(mainFolder, $"Animations/").Replace(@"\", @"/"))), - new XElement("health"), - new XElement("ai")); + if (config == null) + { + config = new XElement("Character", + new XAttribute("speciesname", name), + new XAttribute("humanoid", isHumanoid), + new XElement("ragdolls", CreateRagdollPath()), + new XElement("animations", CreateAnimationPath()), + new XElement("health"), + new XElement("ai")); + } + else + { + config.SetAttributeValue("speciesname", name); + config.SetAttributeValue("humanoid", isHumanoid); + var ragdollElement = config.Element("ragdolls"); + if (ragdollElement == null) + { + config.Add(new XElement("ragdolls", CreateRagdollPath())); + } + else + { + var path = ragdollElement.GetAttributeString("folder", ""); + if (!string.IsNullOrEmpty(path) && !path.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + ragdollElement.ReplaceWith(new XElement("ragdolls", CreateRagdollPath())); + } + } + var animationElement = config.Element("animations"); + if (animationElement == null) + { + config.Add(new XElement("animations", CreateAnimationPath())); + } + else + { + var path = animationElement.GetAttributeString("folder", ""); + if (!string.IsNullOrEmpty(path) && !path.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + animationElement.ReplaceWith(new XElement("animations", CreateAnimationPath())); + } + } + } - XDocument doc = new XDocument(mainElement); + XAttribute CreateRagdollPath() => new XAttribute("folder", Path.Combine(mainFolder, $"Ragdolls/").Replace(@"\", @"/")); + XAttribute CreateAnimationPath() => new XAttribute("folder", Path.Combine(mainFolder, $"Animations/").Replace(@"\", @"/")); + + if (overrideElement != null) + { + overrideElement.Add(config); + config = overrideElement; + } + XDocument doc = new XDocument(config); if (!Directory.Exists(mainFolder)) { Directory.CreateDirectory(mainFolder); @@ -1426,41 +1727,63 @@ namespace Barotrauma // Add to the selected content package contentPackage.AddFile(configFilePath, ContentType.Character); contentPackage.Save(contentPackage.Path); - DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); + DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); + Character.TryAddConfigFile(configFilePath, forceOverride: true); // Ragdoll - string ragdollFolder = RagdollParams.GetFolder(speciesName); - string ragdollPath = RagdollParams.GetDefaultFile(speciesName); + RagdollParams.ClearCache(); + string ragdollPath = RagdollParams.GetDefaultFile(name, contentPackage); RagdollParams ragdollParams = isHumanoid - ? RagdollParams.CreateDefault(ragdollPath, speciesName, ragdollConfig) - : RagdollParams.CreateDefault(ragdollPath, speciesName, ragdollConfig) as RagdollParams; + ? RagdollParams.CreateDefault(ragdollPath, name, ragdoll) + : RagdollParams.CreateDefault(ragdollPath, name, ragdoll) as RagdollParams; + // Animations - string animFolder = AnimationParams.GetFolder(speciesName); - foreach (AnimationType animType in Enum.GetValues(typeof(AnimationType))) + AnimationParams.ClearCache(); + string animFolder = AnimationParams.GetFolder(name, contentPackage); + if (animations != null) { - switch (animType) + if (!Directory.Exists(animFolder)) { - case AnimationType.Walk: - case AnimationType.Run: - if (!ragdollParams.CanEnterSubmarine) { continue; } - break; - case AnimationType.SwimSlow: - case AnimationType.SwimFast: - break; - default: continue; + Directory.CreateDirectory(animFolder); + } + foreach (var animation in animations) + { + XElement element = animation.MainElement; + element.SetAttributeValue("type", name); + string fullPath = AnimationParams.GetDefaultFile(name, animation.AnimationType, contentPackage); + element.Name = AnimationParams.GetDefaultFileName(name, animation.AnimationType); + element.Save(fullPath); + } + } + else + { + foreach (AnimationType animType in Enum.GetValues(typeof(AnimationType))) + { + switch (animType) + { + case AnimationType.Walk: + case AnimationType.Run: + if (!ragdollParams.CanEnterSubmarine) { continue; } + break; + case AnimationType.SwimSlow: + case AnimationType.SwimFast: + break; + default: continue; + } + Type type = AnimationParams.GetParamTypeFromAnimType(animType, isHumanoid); + string fullPath = AnimationParams.GetDefaultFile(name, animType, contentPackage); + AnimationParams.Create(fullPath, name, animType, type); } - Type type = AnimationParams.GetParamTypeFromAnimType(animType, isHumanoid); - string fullPath = AnimationParams.GetDefaultFile(speciesName, animType); - AnimationParams.Create(fullPath, speciesName, animType, type); } if (!AllFiles.Contains(configFilePath)) { AllFiles.Add(configFilePath); } + limbPairEditing = false; SpawnCharacter(configFilePath, ragdollParams); - - editLimbsToggle.Selected = true; + limbsToggle.Selected = true; recalculateColliderToggle.Selected = true; + lockSpriteOriginToggle.Selected = false; selectedLimbs.Add(character.AnimController.Limbs.First()); return true; } @@ -1496,8 +1819,9 @@ namespace Barotrauma private GUIFrame characterSelectionPanel; private GUIFrame fileEditPanel; private GUIFrame modesPanel; - private GUIFrame toolsPanel; + private GUIFrame buttonsPanel; private GUIFrame optionsPanel; + private GUIFrame minorModesPanel; private GUIFrame ragdollControls; private GUIFrame jointControls; @@ -1515,21 +1839,30 @@ namespace Barotrauma private GUIScrollBar spriteSheetZoomBar; private GUITickBox copyJointsToggle; private GUITickBox recalculateColliderToggle; + private GUIFrame resetSpriteOrientationButtonParent; - private GUITickBox jointsToggle; - private GUITickBox editAnimsToggle; - private GUITickBox editLimbsToggle; - private GUITickBox paramsToggle; - private GUITickBox spritesheetToggle; + private GUITickBox characterInfoToggle; private GUITickBox ragdollToggle; + private GUITickBox animsToggle; + private GUITickBox limbsToggle; + private GUITickBox paramsToggle; + private GUITickBox jointsToggle; + private GUITickBox spritesheetToggle; + private GUITickBox skeletonToggle; + private GUITickBox lightsToggle; + private GUITickBox damageModifiersToggle; private GUITickBox ikToggle; + private GUITickBox lockSpriteOriginToggle; + private GUIFrame extraRagdollControls; - private GUIButton duplicateLimbButton; - private GUIButton deleteSelectedButton; private GUIButton createJointButton; + private GUIButton createLimbButton; + private GUIButton deleteSelectedButton; + private GUIButton duplicateLimbButton; private ToggleButton modesToggle; - private ToggleButton toolsToggle; + private ToggleButton minorModesToggle; + private ToggleButton buttonsPanelToggle; private ToggleButton optionsToggle; private ToggleButton characterPanelToggle; private ToggleButton fileEditToggle; @@ -1573,34 +1906,39 @@ namespace Barotrauma Vector2 toggleSize = new Vector2(0.03f, 0.03f); CreateCharacterSelectionPanel(); + CreateMinorModesPanel(toggleSize); CreateModesPanel(toggleSize); - CreateToolsPanel(); + CreateButtonsPanel(); CreateFileEditPanel(); CreateOptionsPanel(toggleSize); CreateContextualControls(); } - private void CreateModesPanel(Vector2 toggleSize) + private void CreateMinorModesPanel(Vector2 toggleSize) { - modesPanel = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.3f), leftArea.RectTransform, Anchor.BottomLeft), style: null, color: panelColor); - var layoutGroup = new GUILayoutGroup(new RectTransform(new Point(modesPanel.Rect.Width - innerMargin.X, modesPanel.Rect.Height - innerMargin.Y), - modesPanel.RectTransform, Anchor.Center)) + minorModesPanel = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.25f), leftArea.RectTransform, Anchor.BottomLeft) + { + RelativeOffset = new Vector2(0, 0.21f) + }, style: null, color: panelColor); + var layoutGroup = new GUILayoutGroup(new RectTransform(new Point(minorModesPanel.Rect.Width - innerMargin.X, minorModesPanel.Rect.Height - innerMargin.Y), + minorModesPanel.RectTransform, Anchor.Center)) { AbsoluteSpacing = 2, Stretch = true }; - - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.06f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ModesPanel"), font: GUI.LargeFont); - // Main modes - editLimbsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditLimbs")) { Selected = editLimbs }; - jointsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditJoints")) { Selected = editJoints }; - editAnimsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditAnimations")) { Selected = editAnimations }; - // Spacing - new GUIFrame(new RectTransform(toggleSize, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; - // Minor modes - ragdollToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowRagdoll")) { Selected = editRagdoll }; + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.06f), layoutGroup.RectTransform), GetCharacterEditorTranslation("MinorModesTitle"), font: GUI.LargeFont); paramsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowParameters")) { Selected = showParamsEditor }; + paramsToggle.OnSelected = box => + { + showParamsEditor = box.Selected; + return true; + }; spritesheetToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowSpriteSheet")) { Selected = showSpritesheet }; + spritesheetToggle.OnSelected = box => + { + showSpritesheet = box.Selected; + return true; + }; showCollidersToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowColliders")) { Selected = showColliders, @@ -1611,48 +1949,74 @@ namespace Barotrauma } }; ikToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditIKTargets")) { Selected = editIK }; - editAnimsToggle.OnSelected = box => + ikToggle.OnSelected = box => + { + editIK = box.Selected; + return true; + }; + skeletonToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("DrawSkeleton")) { Selected = drawSkeleton }; + skeletonToggle.OnSelected = box => + { + drawSkeleton = box.Selected; + return true; + }; + lightsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EnableLights")) { Selected = GameMain.LightManager.LightingEnabled }; + lightsToggle.OnSelected = box => + { + GameMain.LightManager.LightingEnabled = box.Selected; + return true; + }; + damageModifiersToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("DrawDamageModifiers")) { Selected = drawDamageModifiers }; + damageModifiersToggle.OnSelected = box => + { + drawDamageModifiers = box.Selected; + return true; + }; + minorModesToggle = new ToggleButton(new RectTransform(new Vector2(0.125f, 1), minorModesPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left); + } + + private void CreateModesPanel(Vector2 toggleSize) + { + modesPanel = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.2f), leftArea.RectTransform, Anchor.BottomLeft), style: null, color: panelColor); + var layoutGroup = new GUILayoutGroup(new RectTransform(new Point(modesPanel.Rect.Width - innerMargin.X, modesPanel.Rect.Height - innerMargin.Y), + modesPanel.RectTransform, Anchor.Center)) + { + AbsoluteSpacing = 2, + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.06f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ModesPanel"), font: GUI.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 }; + jointsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditJoints")) { Selected = editJoints }; + animsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditAnimations")) { Selected = editAnimations }; + animsToggle.OnSelected = box => { editAnimations = box.Selected; if (editAnimations) { - SetToggle(editLimbsToggle, false); + SetToggle(limbsToggle, false); SetToggle(jointsToggle, false); + SetToggle(ragdollToggle, false); + SetToggle(characterInfoToggle, false); spritesheetToggle.Selected = false; - ClearSelection(); } + ClearSelection(); ResetParamsEditor(); return true; }; - paramsToggle.OnSelected = box => - { - showParamsEditor = box.Selected; - return true; - }; - editLimbsToggle.OnSelected = box => + limbsToggle.OnSelected = box => { editLimbs = box.Selected; if (editLimbs) { - SetToggle(editAnimsToggle, false); + SetToggle(animsToggle, false); SetToggle(jointsToggle, false); + SetToggle(ragdollToggle, false); + SetToggle(characterInfoToggle, false); spritesheetToggle.Selected = true; - ClearSelection(); - } - ResetParamsEditor(); - return true; - }; - ragdollToggle.OnSelected = box => - { - editRagdoll = box.Selected; - if (editRagdoll) - { - if (!editIK) - { - paramsToggle.Selected = true; - } - ClearSelection(); } + ClearSelection(); ResetParamsEditor(); return true; }; @@ -1661,27 +2025,45 @@ namespace Barotrauma editJoints = box.Selected; if (editJoints) { - SetToggle(editLimbsToggle, false); - SetToggle(editAnimsToggle, false); + SetToggle(limbsToggle, false); + SetToggle(animsToggle, false); + SetToggle(ragdollToggle, false); + SetToggle(characterInfoToggle, false); ikToggle.Selected = false; spritesheetToggle.Selected = true; - ClearSelection(); } + ClearSelection(); ResetParamsEditor(); return true; }; - ikToggle.OnSelected = box => + ragdollToggle.OnSelected = box => { - editIK = box.Selected; - if (editIK) + editRagdoll = box.Selected; + if (editRagdoll) { - ragdollToggle.Selected = true; + SetToggle(limbsToggle, false); + SetToggle(animsToggle, false); + SetToggle(jointsToggle, false); + SetToggle(characterInfoToggle, false); + paramsToggle.Selected = true; } + ClearSelection(); + ResetParamsEditor(); return true; }; - spritesheetToggle.OnSelected = box => + characterInfoToggle.OnSelected = box => { - showSpritesheet = box.Selected; + editCharacterInfo = box.Selected; + if (editCharacterInfo) + { + SetToggle(limbsToggle, false); + SetToggle(animsToggle, false); + SetToggle(ragdollToggle, false); + SetToggle(jointsToggle, false); + paramsToggle.Selected = true; + } + ClearSelection(); + ResetParamsEditor(); return true; }; modesToggle = new ToggleButton(new RectTransform(new Vector2(0.125f, 1), modesPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left); @@ -1703,21 +2085,16 @@ namespace Barotrauma toggle.Selected = value; } - private void CreateToolsPanel() + private void CreateButtonsPanel() { - Vector2 buttonSize = new Vector2(1, 0.06f); - toolsPanel = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.15f), leftArea.RectTransform, Anchor.CenterLeft) + buttonsPanel = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.1f), leftArea.RectTransform, Anchor.BottomLeft) { - RelativeOffset = new Vector2(0, 0.1f) + MinSize = new Point(120, 60), + RelativeOffset = new Vector2(0, 0.47f) }, style: null, color: panelColor); - var layoutGroup = new GUILayoutGroup(new RectTransform(new Point(toolsPanel.Rect.Width - innerMargin.X, toolsPanel.Rect.Height - innerMargin.Y), - toolsPanel.RectTransform, Anchor.Center)) - { - AbsoluteSpacing = 2, - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.06f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ToolsPanel"), font: GUI.LargeFont); - var reloadTexturesButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ReloadTextures")); + Vector2 buttonSize = new Vector2(1, 0.45f); + var parent = new GUIFrame(new RectTransform(new Vector2(0.85f, 0.70f), buttonsPanel.RectTransform, Anchor.Center), style: null); + var reloadTexturesButton = new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.TopCenter), GetCharacterEditorTranslation("ReloadTextures")); reloadTexturesButton.OnClicked += (button, userData) => { foreach (var limb in character.AnimController.Limbs) @@ -1729,7 +2106,7 @@ namespace Barotrauma CreateTextures(); return true; }; - new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("RecreateRagdoll")) + new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.BottomCenter), GetCharacterEditorTranslation("RecreateRagdoll")) { ToolTip = GetCharacterEditorTranslation("RecreateRagdollTooltip"), OnClicked = (button, data) => @@ -1739,10 +2116,10 @@ namespace Barotrauma return true; } }; - - toolsToggle = new ToggleButton(new RectTransform(new Vector2(0.125f, 1), toolsPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left); + buttonsPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.125f, 1), buttonsPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left); } + private void CreateOptionsPanel(Vector2 toggleSize) { optionsPanel = new GUIFrame(new RectTransform(new Vector2(1, 0.3f), rightArea.RectTransform, Anchor.Center) @@ -1755,19 +2132,44 @@ namespace Barotrauma AbsoluteSpacing = 2, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.06f), layoutGroup.RectTransform), GetCharacterEditorTranslation("OptionsPanel"), font: GUI.LargeFont); - freezeToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("Freeze")) { Selected = isFreezed }; - var autoFreezeToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AutoFreeze")) { Selected = autoFreeze }; - var limbPairEditToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LimbPairEditing")) + freezeToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("Freeze")) + { + Selected = isFrozen, + OnSelected = box => + { + isFrozen = box.Selected; + return true; + } + }; + new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AutoFreeze")) + { + Selected = autoFreeze, + OnSelected = box => + { + autoFreeze = box.Selected; + return true; + } + }; + new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LimbPairEditing")) { Selected = limbPairEditing, - Enabled = character.IsHumanoid // TODO: remove when limb pair editing works for non-humanoids + Enabled = character.IsHumanoid, + OnSelected = box => + { + limbPairEditing = box.Selected; + return true; + } }; animTestPoseToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AnimationTestPose")) { Selected = character.AnimController.AnimationTestPose, - Enabled = character.IsHumanoid + Enabled = character.IsHumanoid, + OnSelected = box => + { + character.AnimController.AnimationTestPose = box.Selected; + return true; + } }; new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AutoMove")) { @@ -1796,26 +2198,6 @@ namespace Barotrauma return true; } }; - freezeToggle.OnSelected = box => - { - isFreezed = box.Selected; - return true; - }; - autoFreezeToggle.OnSelected = box => - { - autoFreeze = box.Selected; - return true; - }; - limbPairEditToggle.OnSelected = box => - { - limbPairEditing = box.Selected; - return true; - }; - animTestPoseToggle.OnSelected = box => - { - character.AnimController.AnimationTestPose = box.Selected; - return true; - }; optionsToggle = new ToggleButton(new RectTransform(new Vector2(0.1f, 1), optionsPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right); } @@ -1939,22 +2321,45 @@ namespace Barotrauma return true; } }; - //new GUITextBlock(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupSpriteSheet.RectTransform), "Texture scale:", Color.White); - //new GUIScrollBar(new RectTransform(new Point((int)(elementSize.X * 1.75f), textAreaHeight), layoutGroupSpriteSheet.RectTransform), barSize: 0.2f) - //{ - // BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(textureMinScale, textureMaxScale, RagdollParams.TextureScale)), - // Step = 0.01f, - // OnMoved = (scrollBar, value) => - // { - // RagdollParams.TextureScale = MathHelper.Lerp(textureMinScale, textureMaxScale, value); - // return true; - // } - //}; + new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupSpriteSheet.RectTransform), GetCharacterEditorTranslation("Unrestrict")) + { + TextColor = Color.White, + Selected = unrestrictSpritesheet, + OnSelected = (GUITickBox box) => + { + SetSpritesheetRestriction(box.Selected); + return true; + } + }; + resetSpriteOrientationButtonParent = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.025f), centerArea.RectTransform, Anchor.BottomCenter) + { + AbsoluteOffset = new Point(0, -5), + RelativeOffset = new Vector2(-0.05f, 0) + }, style: null) + { + CanBeFocused = false + }; + new GUIButton(new RectTransform(Vector2.One, resetSpriteOrientationButtonParent.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("Reset")) + { + OnClicked = (box, data) => + { + foreach (var limb in selectedLimbs) + { + TryUpdateSubParam(limb.Params, "spriteorientation", float.NaN); + if (limbPairEditing) + { + UpdateOtherLimbs(limb, l => TryUpdateSubParam(l.Params, "spriteorientation", float.NaN)); + } + } + return true; + } + }; // Limb controls limbControls = new GUIFrame(new RectTransform(Vector2.One, centerArea.RectTransform), style: null) { CanBeFocused = false }; var layoutGroupLimbControls = new GUILayoutGroup(new RectTransform(Vector2.One, limbControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false }; - var lockSpriteOriginToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpriteOrigin")) + lockSpriteOriginToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpriteOrigin")) { + TextColor = Color.White, Selected = lockSpriteOrigin, OnSelected = (GUITickBox box) => { @@ -1962,9 +2367,9 @@ namespace Barotrauma return true; } }; - lockSpriteOriginToggle.TextColor = Color.White; - var lockSpritePositionToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpritePosition")) + new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpritePosition")) { + TextColor = Color.White, Selected = lockSpritePosition, OnSelected = (GUITickBox box) => { @@ -1972,9 +2377,9 @@ namespace Barotrauma return true; } }; - lockSpritePositionToggle.TextColor = Color.White; - var lockSpriteSizeToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpriteSize")) + new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpriteSize")) { + TextColor = Color.White, Selected = lockSpriteSize, OnSelected = (GUITickBox box) => { @@ -1982,9 +2387,9 @@ namespace Barotrauma return true; } }; - lockSpriteSizeToggle.TextColor = Color.White; recalculateColliderToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("AdjustCollider")) { + TextColor = Color.White, Selected = recalculateCollider, OnSelected = (GUITickBox box) => { @@ -1993,7 +2398,17 @@ namespace Barotrauma return true; } }; - recalculateColliderToggle.TextColor = Color.White; + new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("OnlyShowSelectedLimbs")) + { + TextColor = Color.White, + Selected = onlyShowSourceRectForSelectedLimbs, + OnSelected = (GUITickBox box) => + { + onlyShowSourceRectForSelectedLimbs = box.Selected; + return true; + } + }; + // Joint controls Point sliderSize = new Point(300, 20); jointControls = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.075f), centerArea.RectTransform), style: null) { CanBeFocused = false }; @@ -2011,7 +2426,7 @@ namespace Barotrauma } }; // Ragdoll controls - ragdollControls = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.25f), centerArea.RectTransform) { AbsoluteOffset = new Point(0, jointControls.Rect.Bottom) }, style: null) { CanBeFocused = false }; + ragdollControls = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.25f), centerArea.RectTransform), style: null) { CanBeFocused = false }; var layoutGroupRagdoll = new GUILayoutGroup(new RectTransform(Vector2.One, ragdollControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false }; var uniformScalingToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupRagdoll.RectTransform), GetCharacterEditorTranslation("UniformScale")) { @@ -2075,8 +2490,7 @@ namespace Barotrauma limbScaleBar.Bar.OnClicked += (button, data) => { RecreateRagdoll(); - RagdollParams.CreateSnapshot(); - ragdollResetRequiresForceLoading = true; + RagdollParams.StoreSnapshot(); return true; }; jointScaleBar.Bar.OnClicked += (button, data) => @@ -2085,29 +2499,25 @@ namespace Barotrauma { RecreateRagdoll(); } - RagdollParams.CreateSnapshot(); - ragdollResetRequiresForceLoading = true; + RagdollParams.StoreSnapshot(); return true; }; - // Ragdoll manipulation - extraRagdollControls = new GUIFrame(new RectTransform(new Point(140, 30), centerArea.RectTransform, Anchor.BottomRight) + Point buttonSize = new Point(140, 30); + int innerMargin = 5; + int outerMargin = 10; + extraRagdollControls = new GUIFrame(new RectTransform(new Point(buttonSize.X + outerMargin * 2, buttonSize.Y * 4 + innerMargin * 3 + outerMargin * 2), centerArea.RectTransform, Anchor.BottomRight) { - RelativeOffset = new Vector2(0.2f, 0.15f) - }, style: null) + AbsoluteOffset = new Point(30, 0) + }, style: null, color: Color.Black) { CanBeFocused = false }; - var extraRagdollLayout = new GUILayoutGroup(new RectTransform(Vector2.One, extraRagdollControls.RectTransform)); - duplicateLimbButton = new GUIButton(new RectTransform(new Point(140, 30), extraRagdollLayout.RectTransform), "Duplicate Limb") + var extraRagdollLayout = new GUILayoutGroup(new RectTransform(new Point(extraRagdollControls.Rect.Width - outerMargin * 2, extraRagdollControls.Rect.Height - outerMargin * 2), extraRagdollControls.RectTransform, anchor: Anchor.Center)) { - OnClicked = (button, data) => - { - CopyLimb(selectedLimbs.FirstOrDefault()); - return true; - } + AbsoluteSpacing = innerMargin }; - deleteSelectedButton = new GUIButton(new RectTransform(new Point(140, 30), extraRagdollLayout.RectTransform), "Delete Selected") + deleteSelectedButton = new GUIButton(new RectTransform(buttonSize, extraRagdollLayout.RectTransform), GetCharacterEditorTranslation("DeleteSelected")) { OnClicked = (button, data) => { @@ -2115,12 +2525,27 @@ namespace Barotrauma return true; } }; - createJointButton = new GUIButton(new RectTransform(new Point(140, 30), extraRagdollLayout.RectTransform), "Create Joint") + duplicateLimbButton = new GUIButton(new RectTransform(buttonSize, extraRagdollLayout.RectTransform), GetCharacterEditorTranslation("DuplicateLimb")) { OnClicked = (button, data) => { - jointCreationMode = !jointCreationMode; - useMouseOffset = false; + CopyLimb(selectedLimbs.FirstOrDefault()); + return true; + } + }; + createJointButton = new GUIButton(new RectTransform(buttonSize, extraRagdollLayout.RectTransform), GetCharacterEditorTranslation("CreateJoint")) + { + OnClicked = (button, data) => + { + ToggleJointCreationMode(); + return true; + } + }; + createLimbButton = new GUIButton(new RectTransform(buttonSize, extraRagdollLayout.RectTransform), GetCharacterEditorTranslation("CreateLimb")) + { + OnClicked = (button, data) => + { + ToggleLimbCreationMode(); return true; } }; @@ -2195,6 +2620,7 @@ namespace Barotrauma default: throw new NotImplementedException(); } + ResetParamsEditor(); return true; }; } @@ -2242,7 +2668,7 @@ namespace Barotrauma }, elementCount: 8, style: null); jobDropDown.ListBox.Color = new Color(jobDropDown.ListBox.Color.R, jobDropDown.ListBox.Color.G, jobDropDown.ListBox.Color.B, byte.MaxValue); jobDropDown.AddItem("None"); - JobPrefab.List.ForEach(j => jobDropDown.AddItem(j.Name, j.Identifier)); + JobPrefab.List.ForEach(j => jobDropDown.AddItem(j.Value.Name, j.Value.Identifier)); jobDropDown.SelectItem(selectedJob); jobDropDown.OnSelected = (component, data) => { @@ -2289,25 +2715,9 @@ namespace Barotrauma // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; - var quickSaveAnimButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("QuickSaveAnimations")); - quickSaveAnimButton.Color = Color.LightGreen; - quickSaveAnimButton.OnClicked += (button, userData) => - { -#if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) - { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), Color.Red, font: GUI.LargeFont); - return false; - } -#endif - AnimParams.ForEach(p => p.Save()); - animationResetRequiresForceLoading = true; - GUI.AddMessage(GetCharacterEditorTranslation("AllAnimationsSaved"), Color.Green, font: GUI.Font); - return true; - }; - var quickSaveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("QuickSaveRagdoll")); - quickSaveRagdollButton.Color = Color.LightGreen; - quickSaveRagdollButton.OnClicked += (button, userData) => + var saveAllButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveButton")); + saveAllButton.Color = Color.LightGreen; + saveAllButton.OnClicked += (button, userData) => { #if !DEBUG if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) @@ -2316,9 +2726,11 @@ namespace Barotrauma return false; } #endif + character.Params.Save(); + GUI.AddMessage(GetCharacterEditorTranslation("CharacterSavedTo").Replace("[path]", CharacterParams.FullPath), Color.Green, font: GUI.Font, lifeTime: 5); character.AnimController.SaveRagdoll(); - ragdollResetRequiresForceLoading = true; - GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.FullPath), Color.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.FullPath), Color.Green, font: GUI.Font, lifeTime: 5); + AnimParams.ForEach(p => p.Save()); return true; }; // Spacing @@ -2330,7 +2742,7 @@ namespace Barotrauma saveRagdollButton.OnClicked += (button, userData) => { var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); - var inputField = new GUITextBox(new RectTransform(new Point(box.Content.Rect.Width, 30), box.Content.RectTransform, Anchor.Center), RagdollParams.Name); + var inputField = new GUITextBox(new RectTransform(new Point(box.Content.Rect.Width, 30), box.Content.RectTransform, Anchor.Center), RagdollParams.Name.RemoveWhitespace()); box.Buttons[0].OnClicked += (b, d) => { box.Close(); @@ -2347,7 +2759,6 @@ namespace Barotrauma } #endif character.AnimController.SaveRagdoll(inputField.Text); - ragdollResetRequiresForceLoading = true; GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.FullPath), Color.Green, font: GUI.Font); box.Close(); return true; @@ -2454,13 +2865,16 @@ namespace Barotrauma var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), elementCount: 4); foreach (object enumValue in Enum.GetValues(typeof(AnimationType))) { - typeDropdown.AddItem(enumValue.ToString(), enumValue); + if (!(enumValue is AnimationType.NotDefined)) + { + typeDropdown.AddItem(enumValue.ToString(), enumValue); + } } AnimationType selectedType = character.AnimController.ForceSelectAnimationType; typeDropdown.OnSelected = (component, data) => { selectedType = (AnimationType)data; - inputField.Text = character.AnimController.GetAnimationParamsFromType(selectedType).Name; + inputField.Text = character.AnimController.GetAnimationParamsFromType(selectedType)?.Name.RemoveWhitespace(); return true; }; typeDropdown.SelectItem(selectedType); @@ -2480,8 +2894,8 @@ namespace Barotrauma } #endif var animParams = character.AnimController.GetAnimationParamsFromType(selectedType); + if (animParams == null) { return true; } animParams.Save(inputField.Text); - animationResetRequiresForceLoading = true; GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", animParams.AnimationType.ToString()).Replace("[path]", animParams.FullPath), Color.Green, font: GUI.Font); ResetParamsEditor(); box.Close(); @@ -2503,7 +2917,10 @@ namespace Barotrauma var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), elementCount: 4); foreach (object enumValue in Enum.GetValues(typeof(AnimationType))) { - typeDropdown.AddItem(enumValue.ToString(), enumValue); + if (!(enumValue is AnimationType.NotDefined)) + { + typeDropdown.AddItem(enumValue.ToString(), enumValue); + } } AnimationType selectedType = character.AnimController.ForceSelectAnimationType; typeDropdown.OnSelected = (component, data) => @@ -2634,54 +3051,19 @@ namespace Barotrauma // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; - var resetAnimButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ResetAnimations")); - resetAnimButton.Color = Color.Red; - resetAnimButton.OnClicked += (button, userData) => + var resetButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ResetButton")); + resetButton.Color = Color.Red; + resetButton.OnClicked += (button, userData) => { + CharacterParams.Reset(true); AnimParams.ForEach(p => p.Reset(true)); - ResetParamsEditor(); - GUI.AddMessage(GetCharacterEditorTranslation("AllAnimationsReset"), Color.WhiteSmoke, font: GUI.Font); - animationResetRequiresForceLoading = false; - return true; - }; - var resetRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ResetRagdoll")); - resetRagdollButton.Color = Color.Red; - resetRagdollButton.OnClicked += (button, userData) => - { - if (ragdollResetRequiresForceLoading) - { - character.AnimController.ResetRagdoll(forceReload: true); - RecreateRagdoll(); - ragdollResetRequiresForceLoading = false; - } - else - { - character.AnimController.ResetRagdoll(forceReload: false); - // For some reason Enumerable.Contains() method does not find the match, threfore the conversion to a list. - var selectedJointParams = selectedJoints.Select(j => j.jointParams).ToList(); - var selectedLimbParams = selectedLimbs.Select(l => l.limbParams).ToList(); - ClearWidgets(); - ClearSelection(); - foreach (var joint in character.AnimController.LimbJoints) - { - if (selectedJointParams.Contains(joint.jointParams)) - { - selectedJoints.Add(joint); - } - } - foreach (var limb in character.AnimController.Limbs) - { - if (selectedLimbParams.Contains(limb.limbParams)) - { - selectedLimbs.Add(limb); - } - } - ResetParamsEditor(); - } - jointCreationMode = false; - closestSelectedLimb = null; + character.AnimController.ResetRagdoll(forceReload: true); + RecreateRagdoll(); + jointCreationMode = JointCreationMode.None; + isDrawingLimb = false; + newLimbRect = Rectangle.Empty; + jointStartLimb = null; CreateGUI(); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollReset"), Color.WhiteSmoke, font: GUI.Font); return true; }; @@ -2691,23 +3073,44 @@ namespace Barotrauma { OnClicked = (button, data) => { - editLimbsToggle.Selected = false; - editAnimsToggle.Selected = false; - spritesheetToggle.Selected = false; - jointsToggle.Selected = false; - paramsToggle.Selected = false; - ragdollToggle.Selected = false; + ResetView(); + Wizard.Instance.SelectTab(Wizard.Tab.Character); + return true; + } + }; + new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("CopyCharacter")) + { + ToolTip = GetCharacterEditorTranslation("CopyCharacterToolTip"), + OnClicked = (button, data) => + { + ResetView(); + CharacterParams.Serialize(); + RagdollParams.Serialize(); + AnimParams.ForEach(a => a.Serialize()); + Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); Wizard.Instance.SelectTab(Wizard.Tab.Character); return true; } }; fileEditToggle = new ToggleButton(new RectTransform(new Vector2(0.1f, 1), fileEditPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right); + + void ResetView() + { + characterInfoToggle.Selected = false; + ragdollToggle.Selected = false; + limbsToggle.Selected = false; + animsToggle.Selected = false; + spritesheetToggle.Selected = false; + jointsToggle.Selected = false; + paramsToggle.Selected = false; + skeletonToggle.Selected = false; + damageModifiersToggle.Selected = false; + } } #endregion #region ToggleButtons - private enum Direction { Left, @@ -2773,6 +3176,7 @@ namespace Barotrauma #endregion #region Params + private CharacterParams CharacterParams => character.Params; private List AnimParams => character.AnimController.AllAnimParams; private AnimationParams CurrentAnimation => character.AnimController.CurrentAnimationParams; private RagdollParams RagdollParams => character.AnimController.RagdollParams; @@ -2780,57 +3184,189 @@ namespace Barotrauma private void ResetParamsEditor() { ParamsEditor.Instance.Clear(); - if (editAnimations) + if (!editRagdoll && !editCharacterInfo && !editJoints && !editLimbs && !editAnimations) { - AnimParams.ForEach(p => p.AddToEditor(ParamsEditor.Instance)); + paramsToggle.Selected = false; + return; + } + if (editCharacterInfo) + { + var mainEditor = ParamsEditor.Instance; + CharacterParams.AddToEditor(mainEditor, space: 10); + var characterEditor = CharacterParams.SerializableEntityEditor; + // Add some space after the title + characterEditor.AddCustomContent(new GUIFrame(new RectTransform(new Point(characterEditor.Rect.Width, 10), characterEditor.RectTransform), style: null) { CanBeFocused = false }, 1); + if (CharacterParams.AI != null) + { + CreateAddButton(CharacterParams.AI.SerializableEntityEditor, () => CharacterParams.AI.TryAddEmptyTarget(out _), GetCharacterEditorTranslation("AddAITarget")); + foreach (var target in CharacterParams.AI.Targets) + { + CreateCloseButton(target.SerializableEntityEditor, () => CharacterParams.AI.RemoveTarget(target)); + } + } + foreach (var emitter in CharacterParams.BloodEmitters) + { + CreateCloseButton(emitter.SerializableEntityEditor, () => CharacterParams.RemoveBloodEmitter(emitter)); + } + foreach (var emitter in CharacterParams.GibEmitters) + { + CreateCloseButton(emitter.SerializableEntityEditor, () => CharacterParams.RemoveGibEmitter(emitter)); + } + foreach (var sound in CharacterParams.Sounds) + { + CreateCloseButton(sound.SerializableEntityEditor, () => CharacterParams.RemoveSound(sound)); + } + foreach (var inventory in CharacterParams.Inventories) + { + var editor = inventory.SerializableEntityEditor; + CreateCloseButton(editor, () => CharacterParams.RemoveInventory(inventory)); + foreach (var item in inventory.Items) + { + CreateCloseButton(item.SerializableEntityEditor, () => inventory.RemoveItem(item)); + } + CreateAddButton(editor, () => inventory.AddItem(), GetCharacterEditorTranslation("AddInventoryItem")); + } + CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddBloodEmitter(), GetCharacterEditorTranslation("AddBloodEmitter")); + CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddGibEmitter(), GetCharacterEditorTranslation("AddGibEmitter")); + CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddSound(), GetCharacterEditorTranslation("AddSound")); + CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddInventory(), GetCharacterEditorTranslation("AddInventory")); + } + else if (editAnimations) + { + character.AnimController.CurrentAnimationParams?.AddToEditor(ParamsEditor.Instance, space: 10); } else { - if (editRagdoll || !editLimbs && !editJoints) + if (editRagdoll) { - RagdollParams.AddToEditor(ParamsEditor.Instance, alsoChildren: false); - RagdollParams.ColliderParams.ForEach(c => c.AddToEditor(ParamsEditor.Instance)); + RagdollParams.AddToEditor(ParamsEditor.Instance, alsoChildren: false, space: 10); + RagdollParams.Colliders.ForEach(c => c.AddToEditor(ParamsEditor.Instance, false, 10)); } - if (editJoints) + else if (editJoints) { - if (selectedJoints.None()) + if (selectedJoints.Any()) { - RagdollParams.Joints.ForEach(jp => jp.AddToEditor(ParamsEditor.Instance)); + selectedJoints.ForEach(j => j.Params.AddToEditor(ParamsEditor.Instance, true, space: 10)); } else { - foreach (var joint in selectedJoints) - { - joint.jointParams.AddToEditor(ParamsEditor.Instance); - } + RagdollParams.Joints.ForEach(jp => jp.AddToEditor(ParamsEditor.Instance, false, space: 10)); } } - if (editLimbs) + else if (editLimbs) { - if (selectedLimbs.None()) - { - foreach (var limb in character.AnimController.Limbs) - { - limb.limbParams.AddToEditor(ParamsEditor.Instance); - if (limb.attack != null) - { - new SerializableEntityEditor(ParamsEditor.Instance.EditorBox.Content.RectTransform, limb.attack, inGame: false, showName: true); - } - } - } - else + if (selectedLimbs.Any()) { foreach (var limb in selectedLimbs) { - limb.limbParams.AddToEditor(ParamsEditor.Instance); - if (limb.attack != null) + var mainEditor = ParamsEditor.Instance; + var limbEditor = limb.Params.SerializableEntityEditor; + limb.Params.AddToEditor(mainEditor, true, space: 0); + foreach (var damageModifier in limb.Params.DamageModifiers) { - new SerializableEntityEditor(ParamsEditor.Instance.EditorBox.Content.RectTransform, limb.attack, inGame: false, showName: true); + CreateCloseButton(damageModifier.SerializableEntityEditor, () => limb.Params.RemoveDamageModifier(damageModifier)); } + if (limb.Params.Sound == null) + { + CreateAddButtonAtLast(mainEditor, () => limb.Params.AddSound(), GetCharacterEditorTranslation("AddSound")); + } + else + { + CreateCloseButton(limb.Params.Sound.SerializableEntityEditor, () => limb.Params.RemoveSound()); + } + if (limb.Params.LightSource == null) + { + CreateAddButtonAtLast(mainEditor, () => limb.Params.AddLight(), GetCharacterEditorTranslation("AddLightSource")); + } + else + { + CreateCloseButton(limb.Params.LightSource.SerializableEntityEditor, () => limb.Params.RemoveLight()); + } + if (limb.Params.Attack == null) + { + CreateAddButtonAtLast(mainEditor, () => limb.Params.AddAttack(), GetCharacterEditorTranslation("AddAttack")); + } + else + { + var attackParams = limb.Params.Attack; + foreach (var affliction in attackParams.Attack.Afflictions) + { + if (attackParams.AfflictionEditors.TryGetValue(affliction.Key, out SerializableEntityEditor afflictionEditor)) + { + CreateCloseButton(afflictionEditor, () => attackParams.RemoveAffliction(affliction.Value)); + } + } + var attackEditor = attackParams.SerializableEntityEditor; + CreateAddButton(attackEditor, () => attackParams.AddNewAffliction(), GetCharacterEditorTranslation("AddAffliction")); + CreateCloseButton(attackEditor, () => limb.Params.RemoveAttack()); + var space = new GUIFrame(new RectTransform(new Point(attackEditor.RectTransform.Rect.Width, 20), attackEditor.RectTransform), style: null, color: ParamsEditor.Color) + { + CanBeFocused = false + }; + attackEditor.AddCustomContent(space, attackEditor.ContentCount); + } + CreateAddButtonAtLast(mainEditor, () => limb.Params.AddDamageModifier(), GetCharacterEditorTranslation("AddDamageModifier")); } } + else + { + character.AnimController.Limbs.ForEach(l => l.Params.AddToEditor(ParamsEditor.Instance, false, space: 10)); + } } } + + void CreateCloseButton(SerializableEntityEditor editor, Action onButtonClicked) + { + var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, 30), editor.RectTransform), style: null) + { + CanBeFocused = false + }; + new GUIButton(new RectTransform(new Vector2(0.08f, 0.8f), parent.RectTransform, Anchor.BottomRight), "X", color: Color.Red) + { + OnClicked = (button, data) => + { + onButtonClicked(); + ResetParamsEditor(); + return true; + } + }; + editor.AddCustomContent(parent, 0); + } + + void CreateAddButtonAtLast(ParamsEditor editor, Action onButtonClicked, string text) + { + var parentFrame = new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, 50), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) + { + CanBeFocused = false + }; + new GUIButton(new RectTransform(new Vector2(0.45f, 0.6f), parentFrame.RectTransform, Anchor.Center), text) + { + OnClicked = (button, data) => + { + onButtonClicked(); + ResetParamsEditor(); + return true; + } + }; + } + + void CreateAddButton(SerializableEntityEditor editor, Action onButtonClicked, string text) + { + var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, 40), editor.RectTransform), style: null) + { + CanBeFocused = false + }; + new GUIButton(new RectTransform(new Vector2(0.45f, 0.6f), parent.RectTransform, Anchor.CenterLeft), text) + { + OnClicked = (button, data) => + { + onButtonClicked(); + ResetParamsEditor(); + return true; + } + }; + editor.AddCustomContent(parent, editor.ContentCount); + } } private void TryUpdateAnimParam(string name, object value) => TryUpdateParam(character.AnimController.CurrentAnimationParams, name, value); @@ -2838,20 +3374,28 @@ namespace Barotrauma private void TryUpdateParam(EditableParams editableParams, string name, object value) { + if (editableParams.SerializableEntityEditor == null) + { + editableParams.AddToEditor(ParamsEditor.Instance); + } if (editableParams.SerializableProperties.TryGetValue(name, out SerializableProperty p)) { - editableParams.SerializableEntityEditor?.UpdateValue(p, value); + editableParams.SerializableEntityEditor.UpdateValue(p, value); } } - private void TryUpdateJointParam(LimbJoint joint, string name, object value) => TryUpdateSubParam(joint.jointParams, name, value); - private void TryUpdateLimbParam(Limb limb, string name, object value) => TryUpdateSubParam(limb.limbParams, name, value); + 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 TryUpdateSubParam(RagdollSubParams ragdollSubParams, string name, object value) + private void TryUpdateSubParam(RagdollParams.SubParam ragdollSubParams, string name, object value) { + if (ragdollSubParams.SerializableEntityEditor == null) + { + ragdollSubParams.AddToEditor(ParamsEditor.Instance); + } if (ragdollSubParams.SerializableProperties.TryGetValue(name, out SerializableProperty p)) { - ragdollSubParams.SerializableEntityEditor?.UpdateValue(p, value); + ragdollSubParams.SerializableEntityEditor.UpdateValue(p, value); } else { @@ -2860,13 +3404,16 @@ namespace Barotrauma { if (subParams.SerializableProperties.TryGetValue(name, out p)) { - subParams.SerializableEntityEditor?.UpdateValue(p, value); + if (subParams.SerializableEntityEditor == null) + { + subParams.AddToEditor(ParamsEditor.Instance); + } + subParams.SerializableEntityEditor.UpdateValue(p, value); } } else { DebugConsole.ThrowError(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name)); - //ragdollParams.SubParams.ForEach(sp => sp.SerializableProperties.ForEach(prop => DebugConsole.ThrowError($"{sp.Name}: sub param field: {prop.Key}"))); } } } @@ -3011,36 +3558,6 @@ namespace Barotrauma } } - private void DrawJointCreationOnSpritesheet(SpriteBatch spriteBatch, Vector2 startPos) - { - // Spritesheet - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 200, GameMain.GraphicsHeight - 200), GetCharacterEditorTranslation("SelectTargetLimbForJointEnd"), Color.White, Color.Black * 0.5f, 10, GUI.Font); - GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, Color.LightGreen, width: 3); - if (targetLimb != null && targetLimb.ActiveSprite != null) - { - GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(targetLimb), Color.LightGreen, thickness: 3); - } - } - - private void DrawJointCreationOnRagdoll(SpriteBatch spriteBatch, Vector2 startPos) - { - // Ragdoll - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 200, GameMain.GraphicsHeight - 200), GetCharacterEditorTranslation("SelectTargetLimbForJointEnd"), Color.White, Color.Black * 0.5f, 10, GUI.Font); - GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, Color.LightGreen, width: 3); - if (targetLimb != null && targetLimb.ActiveSprite != null) - { - var sourceRect = targetLimb.ActiveSprite.SourceRect; - Vector2 size = sourceRect.Size.ToVector2() * Cam.Zoom * targetLimb.Scale * targetLimb.TextureScale; - Vector2 up = VectorExtensions.BackwardFlipped(targetLimb.Rotation); - Vector2 left = up.Right(); - Vector2 limbScreenPos = SimToScreen(targetLimb.SimPosition); - var offset = targetLimb.ActiveSprite.RelativeOrigin.X * left + targetLimb.ActiveSprite.RelativeOrigin.Y * up; - Vector2 center = limbScreenPos + offset; - corners = MathUtils.GetImaginaryRect(corners, up, center, size); - GUI.DrawRectangle(spriteBatch, corners, Color.LightGreen, thickness: 3); - } - } - private void CalculateSpritesheetZoom() { var texture = textures.OrderByDescending(t => t.Width).FirstOrDefault(); @@ -3052,13 +3569,20 @@ namespace Barotrauma float width = texture.Width; float height = textures.Sum(t => t.Height); float margin = 20; - if (height > width) + if (unrestrictSpritesheet) { - spriteSheetMaxZoom = (centerArea.Rect.Bottom - spriteSheetOffsetY - margin) / height; + spriteSheetMaxZoom = (GameMain.GraphicsWidth - spriteSheetOffsetX * 2 - margin - leftArea.Rect.Width) / width; } else { - spriteSheetMaxZoom = (centerArea.Rect.Left - spriteSheetOffsetX - margin) / width; + if (height > width) + { + spriteSheetMaxZoom = (centerArea.Rect.Bottom - spriteSheetOffsetY - margin) / height; + } + else + { + spriteSheetMaxZoom = (centerArea.Rect.Left - spriteSheetOffsetX - margin) / width; + } } spriteSheetMinZoom = spriteSheetMinZoom > spriteSheetMaxZoom ? spriteSheetMaxZoom : 0.25f; spriteSheetZoom = MathHelper.Clamp(1, spriteSheetMinZoom, spriteSheetMaxZoom); @@ -3101,11 +3625,35 @@ namespace Barotrauma { if (editJoints || editLimbs || editIK) { - RagdollParams.CreateSnapshot(); + RagdollParams.StoreSnapshot(); } if (editAnimations) { - CurrentAnimation.CreateSnapshot(); + CurrentAnimation.StoreSnapshot(); + } + } + + private void ToggleJointCreationMode() + { + switch (jointCreationMode) + { + case JointCreationMode.None: + jointCreationMode = JointCreationMode.Select; + SetToggle(spritesheetToggle, true); + break; + case JointCreationMode.Select: + case JointCreationMode.Create: + jointCreationMode = JointCreationMode.None; + break; + } + } + + private void ToggleLimbCreationMode() + { + isDrawingLimb = !isDrawingLimb; + if (isDrawingLimb) + { + SetToggle(spritesheetToggle, true); } } #endregion @@ -3138,14 +3686,14 @@ namespace Barotrauma 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, 100), 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, GUI.Font); } // Widgets for all anims --> Vector2 referencePoint = SimToScreen(head != null ? head.SimPosition: collider.SimPosition); Vector2 drawPos = referencePoint; if (ShowCycleWidget()) { - GetAnimationWidget("CycleSpeed", Color.MediumPurple, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => + GetAnimationWidget("CycleSpeed", Color.MediumPurple, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => { float multiplier = 0.5f; w.tooltip = GetCharacterEditorTranslation("CycleSpeed"); @@ -3192,7 +3740,7 @@ namespace Barotrauma } else { - GetAnimationWidget("MovementSpeed", Color.Turquoise, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => + GetAnimationWidget("MovementSpeed", Color.Turquoise, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => { float multiplier = 0.5f; w.tooltip = GetCharacterEditorTranslation("MovementSpeed"); @@ -3245,13 +3793,14 @@ namespace Barotrauma { // Head angle DrawRadialWidget(spriteBatch, SimToScreen(head.SimPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, - angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true); + angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); // Head position and leaning + Color color = Color.Red; if (animParams.IsGroundedAnimation) { if (humanGroundedParams != null && character.AnimController is HumanoidAnimController humanAnimController) { - GetAnimationWidget("HeadPosition", Color.Red, initMethod: w => + GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("Head"); w.refresh = () => w.DrawPos = SimToScreen(head.SimPosition.X + humanAnimController.HeadLeanAmount * character.AnimController.Dir, head.PullJointWorldAnchorB.Y); @@ -3300,29 +3849,29 @@ namespace Barotrauma { if (isHorizontal) { - GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), Color.Red); + GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color); } else { - GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), Color.Red); + GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color); } } else { - GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), Color.Red); - GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), Color.Red); + GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color); + GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color); } } else if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head.SimPosition), Color.Red); + GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head.SimPosition), color); } }; }).Draw(spriteBatch, deltaTime); } else { - GetAnimationWidget("HeadPosition", Color.Red, initMethod: w => + GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("HeadPosition"); w.refresh = () => w.DrawPos = SimToScreen(head.SimPosition.X, head.PullJointWorldAnchorB.Y); @@ -3336,7 +3885,7 @@ namespace Barotrauma { if (w.IsControlled) { - GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), Color.Red); + GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color); } }; }).Draw(spriteBatch, deltaTime); @@ -3353,14 +3902,14 @@ namespace Barotrauma } // Torso angle DrawRadialWidget(spriteBatch, SimToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White, - angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true); - + angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); + Color color = Color.DodgerBlue; if (animParams.IsGroundedAnimation) { // Torso position and leaning if (humanGroundedParams != null && character.AnimController is HumanoidAnimController humanAnimController) { - GetAnimationWidget("TorsoPosition", Color.DarkRed, initMethod: w => + GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("Torso"); w.refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X + humanAnimController.TorsoLeanAmount * character.AnimController.Dir, torso.PullJointWorldAnchorB.Y); @@ -3409,29 +3958,29 @@ namespace Barotrauma { if (isHorizontal) { - GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), Color.DarkRed); + GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color); } else { - GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), Color.DarkRed); + GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color); } } else { - GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), Color.DarkRed); - GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), Color.DarkRed); + GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color); + GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color); } } else if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(torso.SimPosition), Color.DarkRed); + GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(torso.SimPosition), color); } }; }).Draw(spriteBatch, deltaTime); } else { - GetAnimationWidget("TorsoPosition", Color.DarkRed, initMethod: w => + GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("TorsoPosition"); w.refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y); @@ -3445,7 +3994,7 @@ namespace Barotrauma { if (w.IsControlled) { - GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), Color.DarkRed); + GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color); } }; }).Draw(spriteBatch, deltaTime); @@ -3456,7 +4005,7 @@ namespace Barotrauma if (tail != null && fishParams != null) { DrawRadialWidget(spriteBatch, SimToScreen(tail.SimPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White, - angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true); + angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); } // Foot angle if (foot != null) @@ -3468,21 +4017,21 @@ namespace Barotrauma { if (limb.type != LimbType.LeftFoot && limb.type != LimbType.RightFoot) continue; - if (!fishParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID)) + if (!fishParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) { - fishParams.FootAnglesInRadians[limb.limbParams.ID] = 0.0f; + fishParams.FootAnglesInRadians[limb.Params.ID] = 0.0f; } DrawRadialWidget(spriteBatch, SimToScreen(new Vector2(limb.SimPosition.X, colliderBottom.Y)), - MathHelper.ToDegrees(fishParams.FootAnglesInRadians[limb.limbParams.ID]), + MathHelper.ToDegrees(fishParams.FootAnglesInRadians[limb.Params.ID]), GetCharacterEditorTranslation("FootAngle"), Color.White, angle => { - fishParams.FootAnglesInRadians[limb.limbParams.ID] = MathHelper.ToRadians(angle); + fishParams.FootAnglesInRadians[limb.Params.ID] = MathHelper.ToRadians(angle); TryUpdateAnimParam("footangles", fishParams.FootAngles); }, - circleRadius: 25, rotationOffset: collider.Rotation, clockWise: dir < 0, wrapAnglePi: true); + circleRadius: 25, rotationOffset: collider.Rotation, clockWise: dir < 0, wrapAnglePi: true, autoFreeze: true); } } else if (humanParams != null) @@ -3493,7 +4042,7 @@ namespace Barotrauma // Grounded only if (groundedParams != null) { - GetAnimationWidget("StepSize", Color.LimeGreen, initMethod: w => + GetAnimationWidget("StepSize", Color.LimeGreen, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("StepSize"); w.refresh = () => @@ -3524,7 +4073,7 @@ namespace Barotrauma { if (hand != null || arm != null) { - GetAnimationWidget("HandMoveAmount", Color.LightGreen, initMethod: w => + GetAnimationWidget("HandMoveAmount", Color.LightGreen, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); float offset = 0.1f; @@ -3564,7 +4113,7 @@ namespace Barotrauma Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude(); - var lengthWidget = GetAnimationWidget("WaveLength", Color.NavajoWhite, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var lengthWidget = GetAnimationWidget("WaveLength", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("TailMovementSpeed"); w.refresh = () => w.DrawPos = GetDrawPos(); @@ -3582,7 +4131,7 @@ namespace Barotrauma } }; }); - var amplitudeWidget = GetAnimationWidget("WaveAmplitude", Color.NavajoWhite, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var amplitudeWidget = GetAnimationWidget("WaveAmplitude", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("TailMovementAmount"); w.refresh = () => w.DrawPos = GetControlPoint(); @@ -3621,7 +4170,7 @@ namespace Barotrauma Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude(); - var lengthWidget = GetAnimationWidget("LegMovementSpeed", Color.NavajoWhite, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var lengthWidget = GetAnimationWidget("LegMovementSpeed", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("LegMovementSpeed"); w.refresh = () => w.DrawPos = GetDrawPos(); @@ -3639,7 +4188,7 @@ namespace Barotrauma } }; }); - var amplitudeWidget = GetAnimationWidget("LegMovementAmount", Color.NavajoWhite, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var amplitudeWidget = GetAnimationWidget("LegMovementAmount", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("LegMovementAmount"); w.refresh = () => w.DrawPos = GetControlPoint(); @@ -3664,7 +4213,7 @@ namespace Barotrauma lengthWidget.Draw(spriteBatch, deltaTime); amplitudeWidget.Draw(spriteBatch, deltaTime); // Arms - GetAnimationWidget("HandMoveAmount", Color.LightGreen, initMethod: w => + GetAnimationWidget("HandMoveAmount", Color.LightGreen, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); float offset = 0.4f; @@ -3726,9 +4275,9 @@ namespace Barotrauma Vector2 limbScreenPos = SimToScreen(limb.SimPosition); bool isSelected = selectedLimbs.Contains(limb); corners = GetLimbPhysicRect(limb); - if (isSelected) + if (isSelected && jointStartLimb != limb && jointEndLimb != limb) { - GUI.DrawRectangle(spriteBatch, corners, Color.White, thickness: 3); + GUI.DrawRectangle(spriteBatch, corners, Color.Yellow, thickness: 3); } if (GUI.MouseOn == null && Widget.selectedWidgets.None() && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(corners, PlayerInput.MousePosition)) { @@ -3779,10 +4328,12 @@ namespace Barotrauma private void DrawRagdoll(SpriteBatch spriteBatch, float deltaTime) { bool altDown = PlayerInput.KeyDown(Keys.LeftAlt); - if (!altDown && editJoints && selectedJoints.Any()) + + if (!altDown && editJoints && selectedJoints.Any() && jointCreationMode == JointCreationMode.None) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 200, 250), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 250), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUI.Font); } + foreach (Limb limb in character.AnimController.Limbs) { if (editIK) @@ -3800,7 +4351,7 @@ namespace Barotrauma ResetParamsEditor(); } limb.PullJointWorldAnchorA = ScreenToSim(PlayerInput.MousePosition); - TryUpdateLimbParam(limb, "pullpos", ConvertUnits.ToDisplayUnits(limb.PullJointLocalAnchorA / limb.limbParams.Ragdoll.LimbScale)); + TryUpdateLimbParam(limb, "pullpos", ConvertUnits.ToDisplayUnits(limb.PullJointLocalAnchorA / limb.Params.Ragdoll.LimbScale)); GUI.DrawLine(spriteBatch, SimToScreen(limb.SimPosition), tformedPullPos, Color.MediumPurple); }); } @@ -3829,7 +4380,7 @@ namespace Barotrauma var f = Vector2.Transform(jointPos, Matrix.CreateRotationZ(limb.Rotation)); f.Y = -f.Y; Vector2 tformedJointPos = limbScreenPos + f * Cam.Zoom; - if (editRagdoll) + if (drawSkeleton) { ShapeExtensions.DrawPoint(spriteBatch, limbScreenPos, Color.Black, size: 5); ShapeExtensions.DrawPoint(spriteBatch, limbScreenPos, Color.White, size: 1); @@ -3846,17 +4397,17 @@ namespace Barotrauma { continue; } - var selectionWidget = GetJointSelectionWidget($"{joint.jointParams.Name} selection widget ragdoll", joint); + var selectionWidget = GetJointSelectionWidget($"{joint.Params.Name} selection widget ragdoll", joint); selectionWidget.DrawPos = tformedJointPos; selectionWidget.Draw(spriteBatch, deltaTime); if (selectedJoints.Contains(joint)) { - if (joint.LimitEnabled) + if (joint.LimitEnabled && jointCreationMode == JointCreationMode.None) { - DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: limb.Rotation); + DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: limb.Rotation, holdPosition: true); } // Is the direction inversed incorrectly? - Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation + MathHelper.ToRadians(-RagdollParams.SpritesheetOrientation), 20); + Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation + MathHelper.ToRadians(-joint.LimbB.Params.GetSpriteOrientation()), 20); GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.Magenta, width: 2); var dotSize = new Vector2(5, 5); var rect = new Rectangle((tformedJointPos - dotSize / 2).ToPoint(), dotSize.ToPoint()); @@ -3864,13 +4415,18 @@ namespace Barotrauma //GUI.DrawLine(spriteBatch, tformedJointPos, tformedJointPos + up * 20, Color.White, width: 3); GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow, width: 3); //GUI.DrawRectangle(spriteBatch, inputRect, Color.Red); - GUI.DrawString(spriteBatch, tformedJointPos + new Vector2(dotSize.X, -dotSize.Y) * 2, $"{joint.jointParams.Name} {jointPos.FormatZeroDecimal()}", Color.White, Color.Black * 0.5f); + GUI.DrawString(spriteBatch, tformedJointPos + new Vector2(dotSize.X, -dotSize.Y) * 2, $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}", Color.White, Color.Black * 0.5f); if (PlayerInput.LeftButtonHeld()) { if (!selectionWidget.IsControlled) { continue; } + if (jointCreationMode != JointCreationMode.None) { continue; } if (autoFreeze) { - isFreezed = true; + isFrozen = true; + } + else + { + character.AnimController.Collider.PhysEnabled = false; } Vector2 input = ConvertUnits.ToSimUnits(scaledMouseSpeed) / Cam.Zoom; input.Y = -input.Y; @@ -3925,7 +4481,8 @@ namespace Barotrauma } else { - isFreezed = freezeToggle.Selected; + isFrozen = freezeToggle.Selected; + character.AnimController.Collider.PhysEnabled = true; } } } @@ -4047,12 +4604,6 @@ namespace Barotrauma } } - private void CalculateSpritesheetPosition() - { - //spriteSheetOffsetX = (int)(GameMain.GraphicsWidth * 0.6f); - spriteSheetOffsetX = 20; - } - private void DrawSpritesheetEditor(SpriteBatch spriteBatch, float deltaTime) { int offsetX = spriteSheetOffsetX; @@ -4075,7 +4626,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(offsetX, offsetY), texture.Bounds.Size.ToVector2() * spriteSheetZoom, Color.White); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.ActiveSprite == null || limb.ActiveSprite.FilePath != texturePaths[i]) continue; + if (limb.ActiveSprite == null || limb.ActiveSprite.FilePath != texturePaths[i]) { continue; } Rectangle rect = limb.ActiveSprite.SourceRect; rect.Size = rect.MultiplySize(spriteSheetZoom); rect.Location = rect.Location.Multiply(spriteSheetZoom); @@ -4115,22 +4666,30 @@ namespace Barotrauma { DrawSpritesheetJointEditor(spriteBatch, deltaTime, limb, limbScreenPos); } + bool isMouseOn = rect.Contains(PlayerInput.MousePosition); if (editLimbs) { - GUI.DrawRectangle(spriteBatch, rect, selectedLimbs.Contains(limb) ? Color.Yellow : Color.Red); int widgetSize = 8; int halfSize = widgetSize / 2; Vector2 stringOffset = new Vector2(5, 14); var topLeft = rect.Location.ToVector2(); var topRight = new Vector2(topLeft.X + rect.Width, topLeft.Y); var bottomRight = new Vector2(topRight.X, topRight.Y + rect.Height); - if (selectedLimbs.Contains(limb)) + bool isSelected = selectedLimbs.Contains(limb); + if (jointStartLimb != limb && jointEndLimb != limb) + { + if (isSelected || !onlyShowSourceRectForSelectedLimbs) + { + GUI.DrawRectangle(spriteBatch, rect, isSelected ? Color.Yellow : (isMouseOn ? Color.White : Color.Red)); + } + } + if (isSelected) { var sprite = limb.ActiveSprite; Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2(); Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y); Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height); - var originWidget = GetLimbEditWidget($"{limb.limbParams.ID}_origin", limb, widgetSize, Widget.Shape.Cross, initMethod: w => + var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb, widgetSize, Widget.Shape.Cross, initMethod: w => { w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Origin")}: {sprite.RelativeOrigin.FormatDoubleDecimal()}"; w.refresh(); @@ -4176,7 +4735,7 @@ namespace Barotrauma originWidget.Draw(spriteBatch, deltaTime); if (!lockSpritePosition) { - var positionWidget = GetLimbEditWidget($"{limb.limbParams.ID}_position", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var positionWidget = GetLimbEditWidget($"{limb.Params.ID}_position", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => { w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Position")}: {limb.ActiveSprite.SourceRect.Location}"; w.refresh(); @@ -4233,7 +4792,7 @@ namespace Barotrauma } if (!lockSpriteSize) { - var sizeWidget = GetLimbEditWidget($"{limb.limbParams.ID}_size", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var sizeWidget = GetLimbEditWidget($"{limb.Params.ID}_size", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => { w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Size")}: {limb.ActiveSprite.SourceRect.Size}"; w.refresh(); @@ -4308,7 +4867,16 @@ namespace Barotrauma sizeWidget.Draw(spriteBatch, deltaTime); } } - else if (rect.Contains(PlayerInput.MousePosition) && GUI.MouseOn == null && Widget.selectedWidgets.None()) + else if (isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None()) + { + // TODO: only one limb name should be displayed (needs to be done in a separate loop) + GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f); + } + } + else + { + GUI.DrawRectangle(spriteBatch, rect, isMouseOn ? Color.White : Color.Gray); + if (isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None()) { // TODO: only one limb name should be displayed (needs to be done in a separate loop) GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f); @@ -4364,10 +4932,10 @@ namespace Barotrauma tformedJointPos.Y = -tformedJointPos.Y; tformedJointPos.X *= character.AnimController.Dir; tformedJointPos += limbScreenPos; - var jointSelectionWidget = GetJointSelectionWidget($"{joint.jointParams.Name} selection widget {anchorID}", joint, $"{joint.jointParams.Name} selection widget {otherID}"); + var jointSelectionWidget = GetJointSelectionWidget($"{joint.Params.Name} selection widget {anchorID}", joint, $"{joint.Params.Name} selection widget {otherID}"); jointSelectionWidget.DrawPos = tformedJointPos; jointSelectionWidget.Draw(spriteBatch, deltaTime); - var otherWidget = GetJointSelectionWidget($"{joint.jointParams.Name} selection widget {otherID}", joint, $"{joint.jointParams.Name} selection widget {anchorID}"); + var otherWidget = GetJointSelectionWidget($"{joint.Params.Name} selection widget {otherID}", joint, $"{joint.Params.Name} selection widget {anchorID}"); if (anchorID == "2") { bool isSelected = selectedJoints.Contains(joint); @@ -4379,9 +4947,9 @@ namespace Barotrauma } if (selectedJoints.Contains(joint)) { - if (joint.LimitEnabled) + if (joint.LimitEnabled && jointCreationMode == JointCreationMode.None) { - DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: false, allowPairEditing: true); + DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: false, allowPairEditing: true, holdPosition: false); } if (jointSelectionWidget.IsControlled) { @@ -4440,11 +5008,11 @@ namespace Barotrauma } } - private void DrawJointLimitWidgets(SpriteBatch spriteBatch, Limb limb, LimbJoint joint, Vector2 drawPos, bool autoFreeze, bool allowPairEditing, float rotationOffset = 0) + private void DrawJointLimitWidgets(SpriteBatch spriteBatch, Limb limb, LimbJoint joint, Vector2 drawPos, bool autoFreeze, bool allowPairEditing, bool holdPosition, float rotationOffset = 0) { - rotationOffset += MathHelper.ToRadians(RagdollParams.SpritesheetOrientation); + rotationOffset += limb.Params.GetSpriteOrientation(); Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? Color.LightGreen * 0.5f : Color.Red; - DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.UpperLimit), $"joint.jointParams.Name {GetCharacterEditorTranslation("UpperLimit")}", Color.Cyan, angle => + DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.UpperLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("UpperLimit")}", Color.Cyan, angle => { joint.UpperLimit = MathHelper.ToRadians(angle); ValidateJoint(joint); @@ -4482,8 +5050,8 @@ namespace Barotrauma DrawAngle(20, angleColor, 4); DrawAngle(40, Color.Cyan); GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Cyan, font: GUI.SmallFont); - }, circleRadius: 40, rotationOffset: rotationOffset, displayAngle: false, clockWise: false); - DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.LowerLimit), $"joint.jointParams.Name {GetCharacterEditorTranslation("LowerLimit")}", Color.Yellow, angle => + }, circleRadius: 40, rotationOffset: rotationOffset, displayAngle: false, clockWise: false, holdPosition: holdPosition); + DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.LowerLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("LowerLimit")}", Color.Yellow, angle => { joint.LowerLimit = MathHelper.ToRadians(angle); ValidateJoint(joint); @@ -4521,7 +5089,7 @@ namespace Barotrauma DrawAngle(20, angleColor, 4); DrawAngle(25, Color.Yellow); GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Yellow, font: GUI.SmallFont); - }, circleRadius: 25, rotationOffset: rotationOffset, displayAngle: false, clockWise: false); + }, circleRadius: 25, rotationOffset: rotationOffset, displayAngle: false, clockWise: false, holdPosition: holdPosition); void DrawAngle(float radius, Color color, float thickness = 5) { float angle = joint.UpperLimit - joint.LowerLimit; @@ -4529,11 +5097,86 @@ namespace Barotrauma offset: -rotationOffset - joint.UpperLimit + MathHelper.PiOver2, thickness: thickness); } } + + private void Nudge(Keys key) + { + switch (key) + { + case Keys.Left: + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + if (PlayerInput.KeyDown(Keys.LeftControl)) + { + newRect.Width--; + } + else + { + newRect.X--; + } + UpdateSourceRect(limb, newRect); + } + break; + case Keys.Right: + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + if (PlayerInput.KeyDown(Keys.LeftControl)) + { + newRect.Width++; + } + else + { + newRect.X++; + } + UpdateSourceRect(limb, newRect); + } + break; + case Keys.Down: + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + if (PlayerInput.KeyDown(Keys.LeftControl)) + { + newRect.Height++; + } + else + { + newRect.Y++; + } + UpdateSourceRect(limb, newRect); + } + break; + case Keys.Up: + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + if (PlayerInput.KeyDown(Keys.LeftControl)) + { + newRect.Height--; + } + else + { + newRect.Y--; + } + UpdateSourceRect(limb, newRect); + } + break; + } + RagdollParams.StoreSnapshot(); + } + + private void SetSpritesheetRestriction(bool value) + { + unrestrictSpritesheet = value; + CalculateSpritesheetZoom(); + spriteSheetZoomBar.BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(spriteSheetMinZoom, spriteSheetMaxZoom, spriteSheetZoom)); + } #endregion #region Widgets as methods private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, string 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) + float circleRadius = 30, int widgetSize = 10, float rotationOffset = 0, bool clockWise = true, bool displayAngle = true, bool? autoFreeze = null, bool wrapAnglePi = false, bool holdPosition = false) { var angle = value; if (!MathUtils.IsValid(angle)) @@ -4559,7 +5202,7 @@ namespace Barotrauma onClick(angle); var zeroPos = drawPos + VectorExtensions.ForwardFlipped(rotationOffset, circleRadius); GUI.DrawLine(spriteBatch, drawPos, zeroPos, Color.Red, width: 3); - }, autoFreeze, onHovered: () => + }, autoFreeze, holdPosition, onHovered: () => { if (!PlayerInput.LeftButtonHeld()) { @@ -4570,7 +5213,7 @@ namespace Barotrauma } 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, Action onHovered = null) + 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) { var drawRect = new Rectangle((int)drawPos.X - size / 2, (int)drawPos.Y - size / 2, size, size); var inputRect = drawRect; @@ -4618,13 +5261,18 @@ namespace Barotrauma { if (autoFreeze ?? this.autoFreeze) { - isFreezed = true; + isFrozen = true; + } + if (holdPosition == true) + { + character.AnimController.Collider.PhysEnabled = false; } onPressed(); } else { - isFreezed = freezeToggle.Selected; + isFrozen = freezeToggle.Selected; + character.AnimController.Collider.PhysEnabled = true; } // Might not be entirely reliable, since the method is used inside the draw loop. if (PlayerInput.LeftButtonClicked()) @@ -4640,7 +5288,7 @@ namespace Barotrauma private Dictionary jointSelectionWidgets = new Dictionary(); private Dictionary limbEditWidgets = new Dictionary(); - private Widget GetAnimationWidget(string name, Color color, int size = 10, float sizeMultiplier = 2, Widget.Shape shape = Widget.Shape.Rectangle, Action initMethod = null) + private Widget GetAnimationWidget(string name, Color innerColor, Color? outerColor = null, int size = 10, float sizeMultiplier = 2, Widget.Shape shape = Widget.Shape.Rectangle, Action initMethod = null) { string id = $"{character.SpeciesName}_{character.AnimController.CurrentAnimationParams.AnimationType.ToString()}_{name}"; if (!animationWidgets.TryGetValue(id, out Widget widget)) @@ -4651,8 +5299,9 @@ namespace Barotrauma tooltipOffset = new Vector2(selectedSize / 2 + 5, -10), data = character.AnimController.CurrentAnimationParams }; - widget.MouseUp += () => CurrentAnimation.CreateSnapshot(); - widget.color = color; + widget.MouseUp += () => CurrentAnimation.StoreSnapshot(); + widget.color = innerColor; + widget.secondaryColor = outerColor; widget.PreUpdate += dTime => { widget.Enabled = editAnimations; @@ -4722,6 +5371,7 @@ namespace Barotrauma }; widget.MouseDown += () => { + if (jointCreationMode != JointCreationMode.None) { return; } if (!selectedJoints.Contains(joint)) { if (!Widget.EnableMultiSelect) @@ -4741,8 +5391,14 @@ namespace Barotrauma } ResetParamsEditor(); }; - widget.MouseUp += () => RagdollParams.CreateSnapshot(); - widget.tooltip = joint.jointParams.Name; + widget.MouseUp += () => + { + if (jointCreationMode == JointCreationMode.None) + { + RagdollParams.StoreSnapshot(); + } + }; + widget.tooltip = joint.Params.Name; jointSelectionWidgets.Add(ID, widget); return widget; } @@ -4776,1099 +5432,11 @@ namespace Barotrauma w.size = w.IsSelected ? selectedSize : normalSize; w.isFilled = w.IsControlled; }; - w.MouseUp += () => RagdollParams.CreateSnapshot(); + w.MouseUp += () => RagdollParams.StoreSnapshot(); initMethod?.Invoke(w); return w; } } #endregion - - #region Character Wizard - private class Wizard - { - // Ragdoll data - private string name = string.Empty; - private bool isHumanoid = false; - private bool canEnterSubmarine = true; - private string texturePath; - private string xmlPath; - private ContentPackage contentPackage; - private Dictionary limbXElements = new Dictionary(); - private List limbGUIElements = new List(); - private List jointXElements = new List(); - private List jointGUIElements = new List(); - - public static Wizard instance; - public static Wizard Instance - { - get - { - if (instance == null) - { - instance = new Wizard(); - } - return instance; - } - } - - public void Reset() - { - CharacterView.Get().Release(); - RagdollView.Get().Release(); - instance = null; - } - - public enum Tab { None, Character, Ragdoll } - private View activeView; - private Tab currentTab; - - public void SelectTab(Tab tab) - { - currentTab = tab; - activeView?.Box.Close(); - switch (currentTab) - { - case Tab.Character: - activeView = CharacterView.Get(); - break; - case Tab.Ragdoll: - activeView = RagdollView.Get(); - break; - case Tab.None: - default: - Reset(); - break; - } - } - - public void AddToGUIUpdateList() - { - activeView?.Box.AddToGUIUpdateList(); - } - - private class CharacterView : View - { - private static CharacterView instance; - public static CharacterView Get() => Get(ref instance); - - public override void Release() => instance = null; - - protected override GUIMessageBox Create() - { - var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Next") }, new Vector2(0.5f, 1.0f)); - box.Header.Font = GUI.LargeFont; - box.Content.ChildAnchor = Anchor.TopCenter; - box.Content.AbsoluteSpacing = 20; - int elementSize = 30; - var listBox = new GUIListBox(new RectTransform(new Vector2(1, 0.9f), box.Content.RectTransform)); - var topGroup = new GUILayoutGroup(new RectTransform(Vector2.One, listBox.Content.RectTransform)) { AbsoluteSpacing = 2 }; - var fields = new List(); - GUITextBox texturePathElement = null; - GUITextBox xmlPathElement = null; - GUIDropDown contentPackageDropDown = null; - bool updateTexturePath = true; - void UpdatePaths() - { - string pathBase = ContentPackage == GameMain.VanillaContent ? $"Content/Characters/{Name}/{Name}" - : $"Mods/{(ContentPackage != null ? ContentPackage.Name + "/" : string.Empty)}Characters/{Name}/{Name}"; - XMLPath = $"{pathBase}.xml"; - xmlPathElement.Text = XMLPath; - if (updateTexturePath) - { - TexturePath = $"{pathBase}.png"; - texturePathElement.Text = TexturePath; - } - } - for (int i = 0; i < 6; i++) - { - var mainElement = new GUIFrame(new RectTransform(new Point(topGroup.RectTransform.Rect.Width, elementSize), topGroup.RectTransform), style: null, color: Color.Gray * 0.25f); - fields.Add(mainElement); - RectTransform leftElement = new RectTransform(new Vector2(0.5f, 1), mainElement.RectTransform, Anchor.TopLeft); - RectTransform rightElement = new RectTransform(new Vector2(0.5f, 1), mainElement.RectTransform, Anchor.TopRight); - switch (i) - { - case 0: - new GUITextBlock(leftElement, TextManager.Get("Name")); - var nameField = new GUITextBox(rightElement, GetCharacterEditorTranslation("DefaultName")) { CaretColor = Color.White }; - string ProcessText(string text) => text.RemoveWhitespace().CapitaliseFirstInvariant(); - Name = ProcessText(nameField.Text); - nameField.OnTextChanged += (tb, text) => - { - Name = ProcessText(text); - UpdatePaths(); - return true; - }; - break; - case 1: - new GUITextBlock(leftElement, GetCharacterEditorTranslation("IsHumanoid")) - { - TextColor = Color.White * 0.3f - }; - new GUITickBox(rightElement, string.Empty) - { - Selected = IsHumanoid, - OnSelected = (tB) => IsHumanoid = tB.Selected, - Enabled = false - }; - break; - case 2: - new GUITextBlock(leftElement, GetCharacterEditorTranslation("CanEnterSubmarines")); - new GUITickBox(rightElement, string.Empty) - { - Selected = CanEnterSubmarine, - OnSelected = (tB) => CanEnterSubmarine = tB.Selected - }; - break; - case 3: - new GUITextBlock(leftElement, GetCharacterEditorTranslation("ConfigFileOutput")); - xmlPathElement = new GUITextBox(rightElement, string.Empty) - { - CaretColor = Color.White - }; - xmlPathElement.OnTextChanged += (tb, text) => - { - XMLPath = text; - return true; - }; - break; - case 4: - new GUITextBlock(leftElement, GetCharacterEditorTranslation("TexturePath")); - texturePathElement = new GUITextBox(rightElement, string.Empty) - { - CaretColor = Color.White, - }; - texturePathElement.OnTextChanged += (tb, text) => - { - updateTexturePath = false; - TexturePath = text; - return true; - }; - break; - case 5: - mainElement.RectTransform.NonScaledSize = new Point( - mainElement.RectTransform.NonScaledSize.X, - mainElement.RectTransform.NonScaledSize.Y * 2); - new GUITextBlock(leftElement, TextManager.Get("ContentPackage")); - var rightContainer = new GUIFrame(rightElement, style: null); - contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); - foreach (ContentPackage cp in ContentPackage.List) - { -#if !DEBUG - if (cp == GameMain.VanillaContent) { continue; } -#endif - contentPackageDropDown.AddItem(cp.Name, userData: cp, toolTip: cp.Path); - } - contentPackageDropDown.OnSelected = (obj, userdata) => - { - ContentPackage = userdata as ContentPackage; - updateTexturePath = true; - UpdatePaths(); - return true; - }; - var contentPackageNameElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 0.5f), rightContainer.RectTransform, Anchor.BottomLeft), - TextManager.Get("name")) - { - CaretColor = Color.White, - }; - var createNewPackageButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), rightContainer.RectTransform, Anchor.BottomRight), TextManager.Get("CreateNew")) - { - OnClicked = (btn, userdata) => - { - if (string.IsNullOrEmpty(contentPackageNameElement.Text)) - { - contentPackageNameElement.Flash(); - return false; - } - if (ContentPackage.List.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) - { - new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", fallBackTag: "leveleditorlevelobjnametaken")); - return false; - } - string fileName = ToolBox.RemoveInvalidFileNameChars(contentPackageNameElement.Text); - ContentPackage = ContentPackage.CreatePackage( - contentPackageNameElement.Text, - Path.Combine(ContentPackage.Folder, $"{fileName}.xml"), false); - ContentPackage.List.Add(ContentPackage); - GameMain.Config.SelectedContentPackages.Add(ContentPackage); - contentPackageDropDown.AddItem(ContentPackage.Name, ContentPackage, ContentPackage.Path); - contentPackageDropDown.SelectItem(ContentPackage); - contentPackageNameElement.Text = ""; - return true; - }, - Enabled = false - }; - Color textColor = contentPackageNameElement.TextColor; - contentPackageNameElement.TextColor *= 0.6f; - contentPackageNameElement.OnSelected += (sender, key) => - { - contentPackageNameElement.Text = ""; - }; - contentPackageNameElement.OnTextChanged += (textBox, text) => - { - textBox.TextColor = textColor; - createNewPackageButton.Enabled = !string.IsNullOrWhiteSpace(text); - return true; - }; - break; - } - } - UpdatePaths(); - //var codeArea = new GUIFrame(new RectTransform(new Vector2(1, 0.5f), listBox.Content.RectTransform), style: null) { CanBeFocused = false }; - //new GUITextBlock(new RectTransform(new Vector2(1, 0.05f), codeArea.RectTransform), "Custom code:"); - //var inputBox = new GUITextBox(new RectTransform(new Vector2(1, 1 - 0.05f), codeArea.RectTransform, Anchor.BottomLeft), string.Empty, textAlignment: Alignment.TopLeft); - // Cancel - box.Buttons[0].OnClicked += (b, d) => - { - Wizard.Instance.SelectTab(Tab.None); - return true; - }; - // Next - box.Buttons[1].OnClicked += (b, d) => - { - if (ContentPackage == null) - { - contentPackageDropDown.Flash(); - return false; - } - if (!File.Exists(TexturePath)) - { - GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), Color.Red); - texturePathElement.Flash(Color.Red); - return false; - } - var path = Path.GetFileName(TexturePath); - if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) - { - GUI.AddMessage(TextManager.Get("WrongFileType"), Color.Red); - texturePathElement.Flash(Color.Red); - return false; - } - Wizard.Instance.SelectTab(Tab.Ragdoll); - return true; - }; - return box; - } - } - - private class RagdollView : View - { - private static RagdollView instance; - public static RagdollView Get() => Get(ref instance); - - public override void Release() => instance = null; - - protected override GUIMessageBox Create() - { - var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new string[] { TextManager.Get("Previous"), TextManager.Get("Create") }, new Vector2(0.5f, 1.0f)); - box.Header.Font = GUI.LargeFont; - box.Content.ChildAnchor = Anchor.TopCenter; - box.Content.AbsoluteSpacing = 20; - int elementSize = 30; - var topGroup = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.05f), box.Content.RectTransform)) { AbsoluteSpacing = 2 }; - var bottomGroup = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.75f), box.Content.RectTransform)) { AbsoluteSpacing = 10 }; - // HTML - GUIMessageBox htmlBox = null; - var loadHtmlButton = new GUIButton(new RectTransform(new Point(topGroup.RectTransform.Rect.Width, elementSize), topGroup.RectTransform), GetCharacterEditorTranslation("LoadFromHTML")); - // Limbs - var limbsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), bottomGroup.RectTransform), style: null) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbsElement.RectTransform), $"{GetCharacterEditorTranslation("Limbs")}: "); - var limbButtonElement = new GUIFrame(new RectTransform(new Vector2(0.8f, 1f), limbsElement.RectTransform) - { RelativeOffset = new Vector2(0.1f, 0) }, style: null) { CanBeFocused = false }; - var limbEditLayout = new GUILayoutGroup(new RectTransform(Vector2.One, limbButtonElement.RectTransform), isHorizontal: true) { AbsoluteSpacing = 10 }; - var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), bottomGroup.RectTransform)); - var removeLimbButton = new GUIButton(new RectTransform(new Point(limbButtonElement.Rect.Height, limbButtonElement.Rect.Height), limbEditLayout.RectTransform), "-") - { - OnClicked = (b, d) => - { - var element = LimbGUIElements.LastOrDefault(); - if (element == null) { return false; } - element.RectTransform.Parent = null; - LimbGUIElements.Remove(element); - return true; - } - }; - var addLimbButton = new GUIButton(new RectTransform(new Point(limbButtonElement.Rect.Height, limbButtonElement.Rect.Height), limbEditLayout.RectTransform), "+") - { - OnClicked = (b, d) => - { - LimbType limbType = LimbType.None; - switch (LimbGUIElements.Count) - { - case 0: - limbType = LimbType.Torso; - break; - case 1: - limbType = LimbType.Head; - break; - } - CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: limbType); - return true; - } - }; - - int x = 1, y = 1, w = 100, h = 100; - int otherElements = limbButtonElement.Rect.Width / 4 + 10 + limbButtonElement.Rect.Height * 2 + 10 + limbButtonElement.RectTransform.AbsoluteOffset.X; - var frame = new GUIFrame(new RectTransform(new Point(limbEditLayout.Rect.Width - otherElements, limbButtonElement.Rect.Height), limbEditLayout.RectTransform), color: Color.Transparent); - var inputArea = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - for (int i = 3; i >= 0; i--) - { - var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.rectComponentLabels[i], font: GUI.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 - }; - switch (i) - { - case 0: - case 1: - numberInput.IntValue = 1; - numberInput.MinValueInt = 1; - numberInput.MaxValueInt = 100; - break; - case 2: - case 3: - numberInput.IntValue = 100; - numberInput.MinValueInt = 0; - numberInput.MaxValueInt = 999; - break; - - } - int comp = i; - numberInput.OnValueChanged += (numInput) => - { - switch (comp) - { - case 0: - x = numInput.IntValue; - break; - case 1: - y = numInput.IntValue; - break; - case 2: - w = numInput.IntValue; - break; - case 3: - h = numInput.IntValue; - break; - } - }; - } - - new GUIButton(new RectTransform(new Point(limbButtonElement.Rect.Width / 4, limbButtonElement.Rect.Height), limbEditLayout.RectTransform) - , GetCharacterEditorTranslation("AddMultipleLimbsButton")) - { - OnClicked = (b, d) => - { - for (int i = 0; i < x; i++) - { - for (int j = 0; j < y; j++) - { - LimbType limbType = LimbType.None; - switch (LimbGUIElements.Count) - { - case 0: - limbType = LimbType.Torso; - break; - case 1: - limbType = LimbType.Head; - break; - } - CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: limbType, sourceRect: new Rectangle(i * w, j * h, w, h)); - } - } - return true; - } - }; - // Joints - new GUIFrame(new RectTransform(new Vector2(1, 0.05f), bottomGroup.RectTransform), style: null) { CanBeFocused = false }; - var jointsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), bottomGroup.RectTransform), style: null) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), jointsElement.RectTransform), $"{GetCharacterEditorTranslation("Joints")}: "); - var jointButtonElement = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), jointsElement.RectTransform) - { RelativeOffset = new Vector2(0.1f, 0) }, style: null) { CanBeFocused = false }; - var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), bottomGroup.RectTransform)); - var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), "-") - { - OnClicked = (b, d) => - { - var element = JointGUIElements.LastOrDefault(); - if (element == null) { return false; } - element.RectTransform.Parent = null; - JointGUIElements.Remove(element); - return true; - } - }; - var addJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform) - { - AbsoluteOffset = new Point(removeJointButton.Rect.Width + 10, 0) - }, "+") - { - OnClicked = (b, d) => - { - CreateJointGUIElement(jointsList.Content.RectTransform, elementSize); - return true; - } - }; - loadHtmlButton.OnClicked = (b, d) => - { - if (htmlBox == null) - { - htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new string[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.5f, 1.0f)); - htmlBox.Header.Font = GUI.LargeFont; - var element = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath")); - var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), element.RectTransform, Anchor.TopRight), $"Content/Characters/{Name}/{Name}.html"); - var list = new GUIListBox(new RectTransform(new Vector2(1, 0.8f), htmlBox.Content.RectTransform)); - var htmlOutput = new GUITextBlock(new RectTransform(Vector2.One, list.Content.RectTransform), string.Empty) { CanBeFocused = false }; - htmlBox.Buttons[0].OnClicked += (_b, _d) => - { - htmlBox.Close(); - return true; - }; - htmlBox.Buttons[1].OnClicked += (_b, _d) => - { - LimbGUIElements.ForEach(l => l.RectTransform.Parent = null); - LimbGUIElements.Clear(); - JointGUIElements.ForEach(j => j.RectTransform.Parent = null); - JointGUIElements.Clear(); - LimbXElements.Clear(); - JointXElements.Clear(); - ParseRagdollFromHTML(htmlPathElement.Text, (id, limbName, limbType, rect) => - { - CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id, limbName, limbType, rect); - }, (id1, id2, anchor1, anchor2, jointName) => - { - CreateJointGUIElement(jointsList.Content.RectTransform, elementSize, id1, id2, anchor1, anchor2, jointName); - }); - htmlOutput.Text = new XDocument(new XElement("Ragdoll", new object[] - { - new XAttribute("type", Name), LimbXElements.Values, JointXElements - })).ToString(); - htmlOutput.CalculateHeightFromText(); - list.UpdateScrollBarSize(); - return true; - }; - } - else - { - GUIMessageBox.MessageBoxes.Add(htmlBox); - } - return true; - }; - //var codeArea = new GUIFrame(new RectTransform(new Vector2(1, 0.5f), listBox.Content.RectTransform), style: null) { CanBeFocused = false }; - //new GUITextBlock(new RectTransform(new Vector2(1, 0.05f), codeArea.RectTransform), "Custom code:"); - //new GUITextBox(new RectTransform(new Vector2(1, 1 - 0.05f), codeArea.RectTransform, Anchor.BottomLeft), string.Empty, textAlignment: Alignment.TopLeft); - // Previous - box.Buttons[0].OnClicked += (b, d) => - { - Wizard.Instance.SelectTab(Tab.Character); - return true; - }; - // Parse and create - box.Buttons[1].OnClicked += (b, d) => - { - ParseLimbsFromGUIElements(); - ParseJointsFromGUIElements(); - var torsoAttributes = LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.ToLowerInvariant() == "torso"); - if (torsoAttributes.Count() != 1) - { - GUI.AddMessage(GetCharacterEditorTranslation("MultipleTorsosDefined"), Color.Red); - return false; - } - XElement torso = torsoAttributes.Single().Parent; - int radius = torso.GetAttributeInt("radius", -1); - int height = torso.GetAttributeInt("height", -1); - int width = torso.GetAttributeInt("width", -1); - int colliderHeight = -1; - if (radius == -1) - { - // the collider is a box -> calculate the capsule - if (width == height) - { - radius = width / 2; - colliderHeight = width - radius * 2; - } - else - { - if (height > width) - { - radius = width / 2; - colliderHeight = height - radius * 2; - } - else - { - radius = height / 2; - colliderHeight = width - radius * 2; - } - } - radius = Math.Max(radius, 1); - } - else if (height > -1 || width > -1) - { - // the collider is a capsule -> use the capsule as it is - colliderHeight = width > height ? width : height; - } - var colliderAttributes = new List() { new XAttribute("radius", radius) }; - if (colliderHeight > -1) - { - colliderHeight = Math.Max(colliderHeight, 1); - if (height > width) - { - colliderAttributes.Add(new XAttribute("height", colliderHeight)); - } - else - { - colliderAttributes.Add(new XAttribute("width", colliderHeight)); - } - } - var colliderElements = new List() { new XElement("collider", colliderAttributes) }; - if (IsHumanoid) - { - // For humanoids, we need a secondary, shorter collider for crouching - var secondaryCollider = new XElement("collider", new XAttribute("radius", radius)); - if (colliderHeight > -1) - { - colliderHeight = Math.Max(colliderHeight, 1); - if (height > width) - { - secondaryCollider.Add(new XAttribute("height", colliderHeight * 0.75f)); - } - else - { - secondaryCollider.Add(new XAttribute("width", colliderHeight * 0.75f)); - } - } - colliderElements.Add(secondaryCollider); - } - var ragdollParams = new object[] - { - new XAttribute("type", Name), - new XAttribute("canentersubmarine", CanEnterSubmarine), - colliderElements, - LimbXElements.Values, - JointXElements - }; - if (CharacterEditorScreen.instance.CreateCharacter(Name, Path.GetDirectoryName(XMLPath), IsHumanoid, ContentPackage, ragdollParams)) - { - GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", Name), Color.Green, font: GUI.Font); - } - Wizard.Instance.SelectTab(Tab.None); - return true; - }; - return box; - } - - private void CreateLimbGUIElement(RectTransform parent, int elementSize, int id, string name = "", LimbType limbType = LimbType.None, Rectangle? sourceRect = null) - { - var limbElement = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementSize * 5 + 40), parent), style: null, color: Color.Gray * 0.25f) - { - CanBeFocused = false - }; - var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 2 }; - var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), name); - 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); - 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) - { - MinValueInt = 0, - MaxValueInt = byte.MaxValue, - IntValue = id, - OnValueChanged = numInput => - { - id = numInput.IntValue; - string text = nameField.GetChild().Text; - string t = string.IsNullOrWhiteSpace(text) ? id.ToString() : text; - label.Text = t; - } - }; - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopLeft), TextManager.Get("Name")); - var nameInput = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopRight), name) - { - CaretColor = Color.White, - }; - nameInput.OnTextChanged += (tb, text) => - { - string t = string.IsNullOrWhiteSpace(text) ? id.ToString() : text; - label.Text = t; - return true; - }; - LimbGUIElements.Add(limbElement); - } - - private void CreateJointGUIElement(RectTransform parent, int elementSize, int id1 = 0, int id2 = 1, Vector2? anchor1 = null, Vector2? anchor2 = null, string jointName = "") - { - var jointElement = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementSize * 6 + 40), parent), style: null, color: Color.Gray * 0.25f) - { - CanBeFocused = false - }; - var group = new GUILayoutGroup(new RectTransform(Vector2.One, jointElement.RectTransform)) { AbsoluteSpacing = 2 }; - var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), jointName); - var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopLeft), TextManager.Get("Name")); - var nameInput = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopRight), jointName) - { - CaretColor = Color.White, - }; - nameInput.OnTextChanged += (textB, text) => - { - jointName = text; - label.Text = jointName; - return true; - }; - var limb1Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")); - var limb1InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) - { - MinValueInt = 0, - MaxValueInt = byte.MaxValue, - IntValue = id1 - }; - var limb2Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")); - var limb2InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) - { - MinValueInt = 0, - 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); - label.Text = GetJointName(jointName); - limb1InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); - limb2InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); - JointGUIElements.Add(jointElement); - string GetJointName(string n) => string.IsNullOrWhiteSpace(n) ? $"{GetCharacterEditorTranslation("Joint")} {limb1InputField.IntValue} - {limb2InputField.IntValue}" : n; - } - } - - private abstract class View - { - // Easy accessors to the common data. - public string Name - { - get => Instance.name; - set => Instance.name = value; - } - public bool IsHumanoid - { - get => Instance.isHumanoid; - set => Instance.isHumanoid = value; - } - public bool CanEnterSubmarine - { - get => Instance.canEnterSubmarine; - set => Instance.canEnterSubmarine = value; - } - public ContentPackage ContentPackage - { - get => Instance.contentPackage; - set => Instance.contentPackage = value; - } - public string TexturePath - { - get => Instance.texturePath; - set => Instance.texturePath = value; - } - public string XMLPath - { - get => Instance.xmlPath; - set => Instance.xmlPath = value; - } - public Dictionary LimbXElements - { - get => Instance.limbXElements; - set => Instance.limbXElements = value; - } - public List LimbGUIElements - { - get => Instance.limbGUIElements; - set => Instance.limbGUIElements = value; - } - public List JointXElements - { - get => Instance.jointXElements; - set => Instance.jointXElements = value; - } - public List JointGUIElements - { - get => Instance.jointGUIElements; - set => Instance.jointGUIElements = value; - } - - private GUIMessageBox box; - public GUIMessageBox Box - { - get - { - if (box == null) - { - box = Create(); - } - return box; - } - } - - protected abstract GUIMessageBox Create(); - protected static T Get(ref T instance) where T : View, new() - { - if (instance == null) - { - instance = new T(); - } - return instance; - } - - public abstract void Release(); - - protected void ParseLimbsFromGUIElements() - { - LimbXElements.Clear(); - for (int i = 0; i < LimbGUIElements.Count; i++) - { - var limbGUIElement = LimbGUIElements[i]; - var allChildren = limbGUIElement.GetAllChildren(); - GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; - int id = GetField(GetCharacterEditorTranslation("ID")).Parent.GetChild().IntValue; - string limbName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; - LimbType limbType = (LimbType)GetField(GetCharacterEditorTranslation("LimbType")).Parent.GetChild().SelectedData; - // Reverse, because the elements are created from right to left - var rectInputs = GetField(GetCharacterEditorTranslation("SourceRectangle")).Parent.GetAllChildren().Where(c => c is GUINumberInput).Select(c => c as GUINumberInput).Reverse().ToArray(); - int width = rectInputs[2].IntValue; - int height = rectInputs[3].IntValue; - var colliderAttributes = new List(); - // Capsules/Circles - //if (width == height) - //{ - // colliderAttributes.Add(new XAttribute("radius", (int)(width / 2 * 0.85f))); - //} - //else - //{ - // if (height > width) - // { - // colliderAttributes.Add(new XAttribute("radius", (int)(width / 2 * 0.85f))); - // colliderAttributes.Add(new XAttribute("height",(int) (height - width * 0.85f))); - // } - // else - // { - // colliderAttributes.Add(new XAttribute("radius", (int)(height / 2 * 0.85f))); - // colliderAttributes.Add(new XAttribute("width", (int)(width - height * 0.85f))); - // } - //} - // Rectangles - colliderAttributes.Add(new XAttribute("height", (int)(height * 0.85f))); - colliderAttributes.Add(new XAttribute("width", (int)(width * 0.85f))); - idToCodeName.TryGetValue(id, out string notes); - LimbXElements.Add(id.ToString(), new XElement("limb", - new XAttribute("id", id), - new XAttribute("name", limbName), - new XAttribute("type", limbType.ToString()), - colliderAttributes, - new XElement("sprite", - new XAttribute("texture", TexturePath), - new XAttribute("sourcerect", $"{rectInputs[0].IntValue}, {rectInputs[1].IntValue}, {width}, {height}")), - new XAttribute("notes", null ?? string.Empty) - )); - } - } - - protected void ParseJointsFromGUIElements() - { - JointXElements.Clear(); - for (int i = 0; i < JointGUIElements.Count; i++) - { - var jointGUIElement = JointGUIElements[i]; - var allChildren = jointGUIElement.GetAllChildren(); - GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; - string jointName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; - int limb1ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")).Parent.GetChild().IntValue; - int limb2ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")).Parent.GetChild().IntValue; - // Reverse, because the elements are created from right to left - var anchor1Inputs = GetField(GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "1")).Parent.GetAllChildren().Where(c => c is GUINumberInput).Select(c => c as GUINumberInput).Reverse().ToArray(); - var anchor2Inputs = GetField(GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "2")).Parent.GetAllChildren().Where(c => c is GUINumberInput).Select(c => c as GUINumberInput).Reverse().ToArray(); - JointXElements.Add(new XElement("joint", - new XAttribute("name", jointName), - new XAttribute("limb1", limb1ID), - new XAttribute("limb2", limb2ID), - new XAttribute("limb1anchor", $"{anchor1Inputs[0].FloatValue.Format(2)}, {anchor1Inputs[1].FloatValue.Format(2)}"), - new XAttribute("limb2anchor", $"{anchor2Inputs[0].FloatValue.Format(2)}, {anchor2Inputs[1].FloatValue.Format(2)}"))); - } - } - - Dictionary idToCodeName = new Dictionary(); - protected void ParseRagdollFromHTML(string path, Action limbCallback = null, Action jointCallback = null) - { - // TODO: parse as xml? - //XDocument doc = XMLExtensions.TryLoadXml(path); - //var xElements = doc.Elements().ToArray(); - string html = string.Empty; - try - { - html = File.ReadAllText(path); - } - catch (Exception e) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("FailedToReadHTML").Replace("[path]", path), e); - return; - } - - var lines = html.Split(new string[] { "", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Where(s => s.Contains("left") && s.Contains("top") && s.Contains("width") && s.Contains("height")); - int id = 0; - Dictionary hierarchyToID = new Dictionary(); - Dictionary idToHierarchy = new Dictionary(); - Dictionary idToPositionCode = new Dictionary(); - Dictionary idToName = new Dictionary(); - idToCodeName.Clear(); - foreach (var line in lines) - { - var codeNames = new string(line.SkipWhile(c => c != '>').Skip(1).ToArray()).Split(','); - for (int i = 0; i < codeNames.Length; i++) - { - string codeName = codeNames[i].Trim(); - if (string.IsNullOrWhiteSpace(codeName)) { continue; } - idToCodeName.Add(id, codeName); - string limbName = new string(codeName.SkipWhile(c => c != '_').Skip(1).ToArray()); - if (string.IsNullOrWhiteSpace(limbName)) { continue; } - idToName.Add(id, limbName); - var parts = line.Split(' '); - int ParseToInt(string selector) - { - string part = parts.First(p => p.Contains(selector)); - string s = new string(part.SkipWhile(c => c != ':').Skip(1).TakeWhile(c => char.IsNumber(c)).ToArray()); - int.TryParse(s, out int v); - return v; - }; - // example: 111311cr -> 111311 - string hierarchy = new string(codeName.TakeWhile(c => char.IsNumber(c)).ToArray()); - if (hierarchyToID.ContainsKey(hierarchy)) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("MultipleItemsWithSameHierarchy").Replace("[hierarchy]", hierarchy).Replace("[name]", codeName)); - return; - } - hierarchyToID.Add(hierarchy, id); - idToHierarchy.Add(id, hierarchy); - string positionCode = new string(codeName.SkipWhile(c => char.IsNumber(c)).TakeWhile(c => c != '_').ToArray()); - idToPositionCode.Add(id, positionCode.ToLowerInvariant()); - int x = ParseToInt("left"); - int y = ParseToInt("top"); - int width = ParseToInt("width"); - int height = ParseToInt("height"); - // This is overridden when the data is loaded from the gui fields. - LimbXElements.Add(hierarchy, new XElement("limb", - new XAttribute("id", id), - new XAttribute("name", limbName), - new XAttribute("type", ParseLimbType(limbName).ToString()), - new XElement("sprite", - new XAttribute("texture", TexturePath), - new XAttribute("sourcerect", $"{x}, {y}, {width}, {height}")) - )); - limbCallback?.Invoke(id, limbName, ParseLimbType(limbName), new Rectangle(x, y, width, height)); - id++; - } - } - for (int i = 0; i < id; i++) - { - if (idToHierarchy.TryGetValue(i, out string hierarchy)) - { - if (hierarchy != "0") - { - // NEW LOGIC: if hierarchy length == 1, parent to 0 - // Else parent to the last bone in the current hierarchy (11 is parented to 1, 212 is parented to 21 etc) - string parent = hierarchy.Length > 1 ? hierarchy.Remove(hierarchy.Length - 1, 1) : "0"; - if (hierarchyToID.TryGetValue(parent, out int parentID)) - { - Vector2 anchor1 = Vector2.Zero; - Vector2 anchor2 = Vector2.Zero; - idToName.TryGetValue(parentID, out string parentName); - idToName.TryGetValue(i, out string limbName); - string jointName = $"{GetCharacterEditorTranslation("Joint")} {parentName} - {limbName}"; - if (idToPositionCode.TryGetValue(i, out string positionCode)) - { - float scalar = 0.8f; - if (LimbXElements.TryGetValue(parent, out XElement parentElement)) - { - Rectangle parentSourceRect = parentElement.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float parentWidth = parentSourceRect.Width / 2 * scalar; - float parentHeight = parentSourceRect.Height / 2 * scalar; - switch (positionCode) - { - case "tl": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "tc": // 0, 1 - anchor1 = new Vector2(0, parentHeight); - break; - case "tr": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "cl": // -1, 0 - anchor1 = new Vector2(-parentWidth, 0); - break; - case "cr": // 1, 0 - anchor1 = new Vector2(parentWidth, 0); - break; - case "bl": // -1, -1 - anchor1 = new Vector2(-parentWidth, -parentHeight); - break; - case "bc": // 0, -1 - anchor1 = new Vector2(0, -parentHeight); - break; - case "br": // 1, -1 - anchor1 = new Vector2(parentWidth, -parentHeight); - break; - } - if (LimbXElements.TryGetValue(hierarchy, out XElement element)) - { - Rectangle sourceRect = element.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float width = sourceRect.Width / 2 * scalar; - float height = sourceRect.Height / 2 * scalar; - switch (positionCode) - { - // Inverse - case "tl": - // br - anchor2 = new Vector2(-width, -height); - break; - case "tc": - // bc - anchor2 = new Vector2(0, -height); - break; - case "tr": - // bl - anchor2 = new Vector2(-width, -height); - break; - case "cl": - // cr - anchor2 = new Vector2(width, 0); - break; - case "cr": - // cl - anchor2 = new Vector2(-width, 0); - break; - case "bl": - // tr - anchor2 = new Vector2(-width, height); - break; - case "bc": - // tc - anchor2 = new Vector2(0, height); - break; - case "br": - // tl - anchor2 = new Vector2(-width, height); - break; - } - } - } - } - // This is overridden when the data is loaded from the gui fields. - JointXElements.Add(new XElement("joint", - new XAttribute("name", jointName), - new XAttribute("limb1", parentID), - new XAttribute("limb2", i), - new XAttribute("limb1anchor", $"{anchor1.X.Format(2)}, {anchor1.Y.Format(2)}"), - new XAttribute("limb2anchor", $"{anchor2.X.Format(2)}, {anchor2.Y.Format(2)}") - )); - jointCallback?.Invoke(parentID, i, anchor1, anchor2, jointName); - } - } - } - } - } - - protected LimbType ParseLimbType(string limbName) - { - var limbType = LimbType.None; - string n = limbName.ToLowerInvariant(); - switch (n) - { - case "head": - limbType = LimbType.Head; - break; - case "torso": - limbType = LimbType.Torso; - break; - case "waist": - case "pelvis": - limbType = LimbType.Waist; - break; - case "tail": - limbType = LimbType.Tail; - break; - } - if (limbType == LimbType.None) - { - if (n.Contains("tail")) - { - limbType = LimbType.Tail; - } - else if (n.Contains("arm") && !n.Contains("lower")) - { - if (n.Contains("right")) - { - limbType = LimbType.RightArm; - } - else if (n.Contains("left")) - { - limbType = LimbType.LeftArm; - } - } - else if (n.Contains("hand") || n.Contains("palm")) - { - if (n.Contains("right")) - { - limbType = LimbType.RightHand; - } - else if (n.Contains("left")) - { - limbType = LimbType.LeftHand; - } - } - else if (n.Contains("thigh") || n.Contains("upperleg")) - { - if (n.Contains("right")) - { - limbType = LimbType.RightThigh; - } - else if (n.Contains("left")) - { - limbType = LimbType.LeftThigh; - } - } - else if (n.Contains("shin") || n.Contains("lowerleg")) - { - if (n.Contains("right")) - { - limbType = LimbType.RightLeg; - } - else if (n.Contains("left")) - { - limbType = LimbType.LeftLeg; - } - } - else if (n.Contains("foot")) - { - if (n.Contains("right")) - { - limbType = LimbType.RightFoot; - } - else if (n.Contains("left")) - { - limbType = LimbType.LeftFoot; - } - } - } - return limbType; - } - } - } - #endregion } } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/Wizard.cs new file mode 100644 index 000000000..872733f3c --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/Wizard.cs @@ -0,0 +1,1308 @@ +using Microsoft.Xna.Framework; +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; +using System.Windows.Forms; + +namespace Barotrauma.CharacterEditor +{ + class Wizard + { + // Ragdoll data + private string name; + private bool isHumanoid; + private bool canEnterSubmarine = true; + private string texturePath; + private string xmlPath; + private ContentPackage contentPackage; + private Dictionary limbXElements = new Dictionary(); + private List limbGUIElements = new List(); + private List jointXElements = new List(); + private List jointGUIElements = new List(); + + public bool IsCopy { get; private set; } + public CharacterParams SourceCharacter { get; private set; } + public RagdollParams SourceRagdoll { get; private set; } + public IEnumerable SourceAnimations { get; private set; } + + public void CopyExisting(CharacterParams character, RagdollParams ragdoll, IEnumerable animations) + { + IsCopy = true; + SourceCharacter = character; + SourceRagdoll = ragdoll; + SourceAnimations = animations; + name = character.SpeciesName; + isHumanoid = character.Humanoid; + canEnterSubmarine = ragdoll.CanEnterSubmarine; + texturePath = ragdoll.Texture; + } + + public static Wizard instance; + public static Wizard Instance + { + get + { + if (instance == null) + { + instance = new Wizard(); + } + return instance; + } + } + + public static string GetCharacterEditorTranslation(string text) => CharacterEditorScreen.GetCharacterEditorTranslation(text); + + public void Reset() + { + CharacterView.Get().Release(); + RagdollView.Get().Release(); + instance = null; + } + + public enum Tab { None, Character, Ragdoll } + private View activeView; + private Tab currentTab; + + public void SelectTab(Tab tab) + { + currentTab = tab; + activeView?.Box.Close(); + switch (currentTab) + { + case Tab.Character: + activeView = CharacterView.Get(); + break; + case Tab.Ragdoll: + activeView = RagdollView.Get(); + break; + case Tab.None: + default: + Reset(); + break; + } + } + + public void AddToGUIUpdateList() + { + activeView?.Box.AddToGUIUpdateList(); + } + + public void CreateCharacter(XElement ragdollElement, XElement characterElement = null, IEnumerable animations = null) + { + if (Character.ConfigFiles.Any(f => (f.Root.IsOverride() ? f.Root.FirstElement() : f.Root).GetAttributeString("speciesname", "").Equals(name, StringComparison.OrdinalIgnoreCase))) + { + 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") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (_, userdata) => + { + msgBox.Close(); + if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) + { + GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name), Color.Green, font: GUI.Font); + } + Wizard.Instance.SelectTab(Tab.None); + return true; + }; + //msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = (_, userdata) => + { + msgBox.Close(); + return true; + }; + } + else + { + if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) + { + GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name), Color.Green, font: GUI.Font); + } + Wizard.Instance.SelectTab(Tab.None); + } + } + + private class CharacterView : View + { + private static CharacterView instance; + public static CharacterView Get() => Get(ref instance); + + public override void Release() => instance = null; + + protected override GUIMessageBox Create() + { + var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new string[] { TextManager.Get("Cancel"), IsCopy ? TextManager.Get("Create") : TextManager.Get("Next") }, new Vector2(0.65f, 1f)); + box.Header.Font = GUI.LargeFont; + box.Content.ChildAnchor = Anchor.TopCenter; + box.Content.AbsoluteSpacing = 20; + int elementSize = 30; + var frame = new GUIFrame(new RectTransform(new Point(box.Content.Rect.Width - (int)(80 * GUI.xScale), box.Content.Rect.Height - (int)(100 * GUI.yScale)), + box.Content.RectTransform, Anchor.Center), style: null, color: ParamsEditor.Color) + { + CanBeFocused = false + }; + var topGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 1), frame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 2 }; + var fields = new List(); + GUITextBox texturePathElement = null; + GUITextBox xmlPathElement = null; + GUIDropDown contentPackageDropDown = null; + bool updateTexturePath = !IsCopy; + bool isTextureSelected = false; + void UpdatePaths() + { + string pathBase = ContentPackage == GameMain.VanillaContent ? $"Content/Characters/{Name}/{Name}" + : $"Mods/{(ContentPackage != null ? ContentPackage.Name + "/" : string.Empty)}Characters/{Name}/{Name}"; + XMLPath = $"{pathBase}.xml"; + xmlPathElement.Text = XMLPath; + if (updateTexturePath) + { + TexturePath = $"{pathBase}.png"; + texturePathElement.Text = TexturePath; + } + } + for (int i = 0; i < 6; i++) + { + var mainElement = new GUIFrame(new RectTransform(new Point(topGroup.RectTransform.Rect.Width, elementSize), topGroup.RectTransform), style: null, color: Color.Gray * 0.25f); + fields.Add(mainElement); + RectTransform leftElement = new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.TopLeft); + RectTransform rightElement = new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.TopRight); + switch (i) + { + case 0: + new GUITextBlock(leftElement, TextManager.Get("Name")); + var nameField = new GUITextBox(rightElement, Name ?? GetCharacterEditorTranslation("DefaultName")) { CaretColor = Color.White }; + string ProcessText(string text) => text.RemoveWhitespace().CapitaliseFirstInvariant(); + Name = ProcessText(nameField.Text); + nameField.OnTextChanged += (tb, text) => + { + Name = ProcessText(text); + UpdatePaths(); + return true; + }; + break; + case 1: + var label = new GUITextBlock(leftElement, GetCharacterEditorTranslation("IsHumanoid")); + var tickBox = new GUITickBox(rightElement, string.Empty) + { + Selected = IsHumanoid, + Enabled = !IsCopy, + OnSelected = (tB) => IsHumanoid = tB.Selected + }; + if (!tickBox.Enabled) + { + label.TextColor *= 0.6f; + } + break; + case 2: + var l = new GUITextBlock(leftElement, GetCharacterEditorTranslation("CanEnterSubmarines")); + var t = new GUITickBox(rightElement, string.Empty) + { + Selected = CanEnterSubmarine, + Enabled = !IsCopy, + OnSelected = (tB) => CanEnterSubmarine = tB.Selected + }; + if (!t.Enabled) + { + l.TextColor *= 0.6f; + } + break; + case 3: + new GUITextBlock(leftElement, GetCharacterEditorTranslation("ConfigFileOutput")); + xmlPathElement = new GUITextBox(rightElement, string.Empty) + { + Text = XMLPath, + CaretColor = Color.White + }; + xmlPathElement.OnTextChanged += (tb, text) => + { + XMLPath = text; + return true; + }; + break; + case 4: + //new GUITextBlock(leftElement, GetCharacterEditorTranslation("TexturePath")); + texturePathElement = new GUITextBox(rightElement, string.Empty) + { + Text = TexturePath, + CaretColor = Color.White, + }; + texturePathElement.OnTextChanged += (tb, text) => + { + updateTexturePath = false; + TexturePath = text; + return true; + }; + string title = GetCharacterEditorTranslation("SelectTexture"); + new GUIButton(leftElement, title) + { + OnClicked = (button, data) => + { + OpenFileDialog ofd = new OpenFileDialog() + { + InitialDirectory = Path.GetFullPath("Mods"), + Filter = "PNG file|*.png", + Title = title + }; + if (ofd.ShowDialog() == DialogResult.OK) + { + isTextureSelected = true; + texturePathElement.Text = ToolBox.ConvertAbsoluteToRelativePath(ofd.FileName); + } + return true; + } + }; + break; + case 5: + mainElement.RectTransform.NonScaledSize = new Point( + mainElement.RectTransform.NonScaledSize.X, + mainElement.RectTransform.NonScaledSize.Y * 2); + new GUITextBlock(leftElement, TextManager.Get("ContentPackage")); + var rightContainer = new GUIFrame(rightElement, style: null); + contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); + foreach (ContentPackage cp in ContentPackage.List) + { +#if !DEBUG + if (cp == GameMain.VanillaContent) { continue; } +#endif + contentPackageDropDown.AddItem(cp.Name, userData: cp, toolTip: cp.Path); + } + contentPackageDropDown.OnSelected = (obj, userdata) => + { + ContentPackage = userdata as ContentPackage; + updateTexturePath = !isTextureSelected && !IsCopy; + UpdatePaths(); + return true; + }; + contentPackageDropDown.Select(0); + var contentPackageNameElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 0.5f), rightContainer.RectTransform, Anchor.BottomLeft), + GetCharacterEditorTranslation("NewContentPackage")) + { + CaretColor = Color.White, + }; + var createNewPackageButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.5f), rightContainer.RectTransform, Anchor.BottomRight), TextManager.Get("CreateNew")) + { + OnClicked = (btn, userdata) => + { + if (string.IsNullOrEmpty(contentPackageNameElement.Text)) + { + contentPackageNameElement.Flash(); + return false; + } + if (ContentPackage.List.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) + { + new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", fallBackTag: "leveleditorlevelobjnametaken")); + return false; + } + string modName = ToolBox.RemoveInvalidFileNameChars(contentPackageNameElement.Text); + ContentPackage = ContentPackage.CreatePackage(contentPackageNameElement.Text, Path.Combine("Mods", modName, Steam.SteamManager.MetadataFileName), false); + ContentPackage.List.Add(ContentPackage); + GameMain.Config.SelectContentPackage(ContentPackage); + contentPackageDropDown.AddItem(ContentPackage.Name, ContentPackage, ContentPackage.Path); + contentPackageDropDown.SelectItem(ContentPackage); + contentPackageNameElement.Text = ""; + return true; + }, + Enabled = false + }; + Color textColor = contentPackageNameElement.TextColor; + contentPackageNameElement.TextColor *= 0.6f; + contentPackageNameElement.OnSelected += (sender, key) => + { + contentPackageNameElement.Text = ""; + }; + contentPackageNameElement.OnTextChanged += (textBox, text) => + { + textBox.TextColor = textColor; + createNewPackageButton.Enabled = !string.IsNullOrWhiteSpace(text); + return true; + }; + break; + } + } + UpdatePaths(); + // Cancel + box.Buttons[0].OnClicked += (b, d) => + { + Wizard.Instance.SelectTab(Tab.None); + return true; + }; + // Next + box.Buttons[1].OnClicked += (b, d) => + { + if (ContentPackage == null) + { + contentPackageDropDown.Flash(); + return false; + } + if (!File.Exists(TexturePath)) + { + GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), Color.Red); + texturePathElement.Flash(Color.Red); + return false; + } + var path = Path.GetFileName(TexturePath); + if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + { + GUI.AddMessage(TextManager.Get("WrongFileType"), Color.Red); + texturePathElement.Flash(Color.Red); + return false; + } + if (IsCopy) + { + SourceRagdoll.Texture = TexturePath; + SourceRagdoll.CanEnterSubmarine = CanEnterSubmarine; + SourceRagdoll.Serialize(); + Wizard.Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); + } + else + { + Wizard.Instance.SelectTab(Tab.Ragdoll); + } + return true; + }; + return box; + } + } + + private class RagdollView : View + { + private static RagdollView instance; + public static RagdollView Get() => Get(ref instance); + + public override void Release() => instance = null; + + protected override GUIMessageBox Create() + { + var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new string[] { TextManager.Get("Previous"), TextManager.Get("Create") }, new Vector2(0.65f, 1f)); + box.Header.Font = GUI.LargeFont; + box.Content.ChildAnchor = Anchor.TopCenter; + box.Content.AbsoluteSpacing = 20; + int elementSize = 30; + var frame = new GUIFrame(new RectTransform(new Point(box.Content.Rect.Width - (int)(80 * GUI.xScale), box.Content.Rect.Height - (int)(200 * GUI.yScale)), + box.Content.RectTransform, Anchor.Center), style: null, color: ParamsEditor.Color) + { + CanBeFocused = false + }; + var topGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.05f), frame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.TopCenter) { AbsoluteSpacing = 2 }; + var bottomGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), frame.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { AbsoluteSpacing = 10 }; + // HTML + GUIMessageBox htmlBox = null; + var loadHtmlButton = new GUIButton(new RectTransform(new Point(topGroup.Rect.Width / 3, elementSize), topGroup.RectTransform), GetCharacterEditorTranslation("LoadFromHTML")); + // Limbs + var limbsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), bottomGroup.RectTransform), style: null) { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbsElement.RectTransform), $"{GetCharacterEditorTranslation("Limbs")}: "); + var limbButtonElement = new GUIFrame(new RectTransform(new Vector2(0.8f, 1f), limbsElement.RectTransform) + { RelativeOffset = new Vector2(0.1f, 0) }, style: null) + { CanBeFocused = false }; + var limbEditLayout = new GUILayoutGroup(new RectTransform(Vector2.One, limbButtonElement.RectTransform), isHorizontal: true) { AbsoluteSpacing = 10 }; + var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), bottomGroup.RectTransform)); + var removeLimbButton = new GUIButton(new RectTransform(new Point(limbButtonElement.Rect.Height, limbButtonElement.Rect.Height), limbEditLayout.RectTransform), "-") + { + OnClicked = (b, d) => + { + var element = LimbGUIElements.LastOrDefault(); + if (element == null) { return false; } + element.RectTransform.Parent = null; + LimbGUIElements.Remove(element); + return true; + } + }; + var addLimbButton = new GUIButton(new RectTransform(new Point(limbButtonElement.Rect.Height, limbButtonElement.Rect.Height), limbEditLayout.RectTransform), "+") + { + OnClicked = (b, d) => + { + LimbType limbType = LimbType.None; + switch (LimbGUIElements.Count) + { + case 0: + limbType = LimbType.Torso; + break; + case 1: + limbType = LimbType.Head; + break; + } + CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: limbType); + return true; + } + }; + + int _x = 1, _y = 1, w = 100, h = 100; + int otherElements = limbButtonElement.Rect.Width / 4 + 10 + limbButtonElement.Rect.Height * 2 + 10 + limbButtonElement.RectTransform.AbsoluteOffset.X; + frame = new GUIFrame(new RectTransform(new Point(limbEditLayout.Rect.Width - otherElements, limbButtonElement.Rect.Height), limbEditLayout.RectTransform), color: Color.Transparent); + var inputArea = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + for (int i = 3; i >= 0; i--) + { + var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.rectComponentLabels[i], font: GUI.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 + }; + switch (i) + { + case 0: + case 1: + numberInput.IntValue = 1; + numberInput.MinValueInt = 1; + numberInput.MaxValueInt = 100; + break; + case 2: + case 3: + numberInput.IntValue = 100; + numberInput.MinValueInt = 0; + numberInput.MaxValueInt = 999; + break; + + } + int comp = i; + numberInput.OnValueChanged += (numInput) => + { + switch (comp) + { + case 0: + _x = numInput.IntValue; + break; + case 1: + _y = numInput.IntValue; + break; + case 2: + w = numInput.IntValue; + break; + case 3: + h = numInput.IntValue; + break; + } + }; + } + new GUIButton(new RectTransform(new Point(limbButtonElement.Rect.Width / 4, limbButtonElement.Rect.Height), limbEditLayout.RectTransform) + , GetCharacterEditorTranslation("AddMultipleLimbsButton")) + { + OnClicked = (b, d) => + { + CreateMultipleLimbs(_x, _y); + return true; + } + }; + // If no elements are defined, create some as default + if (LimbGUIElements.None()) + { + if (IsHumanoid) + { + CreateMultipleLimbs(2, 6); + // Create the missing waist (13th element) + CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: LimbType.Waist, sourceRect: new Rectangle(_x, h * LimbGUIElements.Count / 2, w, h)); + } + else + { + CreateMultipleLimbs(1, 2); + } + } + void CreateMultipleLimbs(int x, int y) + { + for (int i = 0; i < x; i++) + { + for (int j = 0; j < y; j++) + { + LimbType limbType = LimbType.None; + switch (LimbGUIElements.Count) + { + case 0: + limbType = LimbType.Torso; + break; + case 1: + limbType = LimbType.Head; + break; + } + if (IsHumanoid) + { + switch (LimbGUIElements.Count) + { + case 2: + limbType = LimbType.LeftArm; + break; + case 3: + limbType = LimbType.LeftHand; + break; + case 4: + limbType = LimbType.RightArm; + break; + case 5: + limbType = LimbType.RightHand; + break; + case 6: + limbType = LimbType.LeftThigh; + break; + case 7: + limbType = LimbType.LeftLeg; + break; + case 8: + limbType = LimbType.LeftFoot; + break; + case 9: + limbType = LimbType.RightThigh; + break; + case 10: + limbType = LimbType.RightLeg; + break; + case 11: + limbType = LimbType.RightFoot; + break; + case 12: + limbType = LimbType.Waist; + break; + } + } + CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: limbType, sourceRect: new Rectangle(i * w, j * h, w, h)); + } + } + } + // Joints + new GUIFrame(new RectTransform(new Vector2(1, 0.05f), bottomGroup.RectTransform), style: null) { CanBeFocused = false }; + var jointsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), bottomGroup.RectTransform), style: null) { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), jointsElement.RectTransform), $"{GetCharacterEditorTranslation("Joints")}: "); + var jointButtonElement = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), jointsElement.RectTransform) + { RelativeOffset = new Vector2(0.1f, 0) }, style: null) + { CanBeFocused = false }; + var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), bottomGroup.RectTransform)); + var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), "-") + { + OnClicked = (b, d) => + { + var element = JointGUIElements.LastOrDefault(); + if (element == null) { return false; } + element.RectTransform.Parent = null; + JointGUIElements.Remove(element); + return true; + } + }; + var addJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform) + { + AbsoluteOffset = new Point(removeJointButton.Rect.Width + 10, 0) + }, "+") + { + OnClicked = (b, d) => + { + CreateJointGUIElement(jointsList.Content.RectTransform, elementSize); + return true; + } + }; + loadHtmlButton.OnClicked = (b, d) => + { + if (htmlBox == null) + { + htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new string[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); + htmlBox.Header.Font = GUI.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"); + new GUIButton(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), title) + { + OnClicked = (button, data) => + { + OpenFileDialog ofd = new OpenFileDialog() + { + InitialDirectory = Path.GetFullPath("Mods"), + Filter = "HTML file|*.html", + Title = title + }; + if (ofd.ShowDialog() == DialogResult.OK) + { + htmlPathElement.Text = ofd.FileName; + } + return true; + } + }; + var list = new GUIListBox(new RectTransform(new Vector2(1, 0.8f), htmlBox.Content.RectTransform)); + var htmlOutput = new GUITextBlock(new RectTransform(Vector2.One, list.Content.RectTransform), string.Empty) { CanBeFocused = false }; + htmlBox.Buttons[0].OnClicked += (_b, _d) => + { + htmlBox.Close(); + return true; + }; + htmlBox.Buttons[1].OnClicked += (_b, _d) => + { + LimbGUIElements.ForEach(l => l.RectTransform.Parent = null); + LimbGUIElements.Clear(); + JointGUIElements.ForEach(j => j.RectTransform.Parent = null); + JointGUIElements.Clear(); + LimbXElements.Clear(); + JointXElements.Clear(); + ParseRagdollFromHTML(htmlPathElement.Text, (id, limbName, limbType, rect) => + { + CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id, limbName, limbType, rect); + }, (id1, id2, anchor1, anchor2, jointName) => + { + CreateJointGUIElement(jointsList.Content.RectTransform, elementSize, id1, id2, anchor1, anchor2, jointName); + }); + htmlOutput.Text = new XDocument(new XElement("Ragdoll", new object[] + { + new XAttribute("type", Name), LimbXElements.Values, JointXElements + })).ToString(); + htmlOutput.CalculateHeightFromText(); + list.UpdateScrollBarSize(); + return true; + }; + } + else + { + GUIMessageBox.MessageBoxes.Add(htmlBox); + } + return true; + }; + // Previous + box.Buttons[0].OnClicked += (b, d) => + { + Wizard.Instance.SelectTab(Tab.Character); + return true; + }; + // Parse and create + box.Buttons[1].OnClicked += (b, d) => + { + ParseLimbsFromGUIElements(); + ParseJointsFromGUIElements(); + var main = LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.ToLowerInvariant() == "torso").FirstOrDefault() ?? + LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.ToLowerInvariant() == "head").FirstOrDefault(); + if (main == null) + { + GUI.AddMessage(GetCharacterEditorTranslation("MissingTorsoOrHead"), Color.Red); + return false; + } + if (IsHumanoid) + { + if (!IsValid(LimbXElements.Values, true, out string missingType)) + { + GUI.AddMessage(GetCharacterEditorTranslation("MissingLimbType").Replace("[limbtype]", missingType.FormatCamelCaseWithSpaces()), Color.Red); + return false; + } + } + XElement mainLimb = main.Parent; + int radius = mainLimb.GetAttributeInt("radius", -1); + int height = mainLimb.GetAttributeInt("height", -1); + int width = mainLimb.GetAttributeInt("width", -1); + int colliderHeight = -1; + if (radius == -1) + { + // the collider is a box -> calculate the capsule + if (width == height) + { + radius = width / 2; + colliderHeight = width - radius * 2; + } + else + { + if (height > width) + { + radius = width / 2; + colliderHeight = height - radius * 2; + } + else + { + radius = height / 2; + colliderHeight = width - radius * 2; + } + } + radius = Math.Max(radius, 1); + } + else if (height > -1 || width > -1) + { + // the collider is a capsule -> use the capsule as it is + colliderHeight = width > height ? width : height; + } + var colliderAttributes = new List() { new XAttribute("radius", radius) }; + if (colliderHeight > -1) + { + colliderHeight = Math.Max(colliderHeight, 1); + if (height > width) + { + colliderAttributes.Add(new XAttribute("height", colliderHeight)); + } + else + { + colliderAttributes.Add(new XAttribute("width", colliderHeight)); + } + } + var colliderElements = new List() { new XElement("collider", colliderAttributes) }; + if (IsHumanoid) + { + // For humanoids, we need a secondary, shorter collider for crouching + var secondaryCollider = new XElement("collider", new XAttribute("radius", radius)); + if (colliderHeight > -1) + { + colliderHeight = Math.Max(colliderHeight, 1); + if (height > width) + { + secondaryCollider.Add(new XAttribute("height", colliderHeight * 0.75f)); + } + else + { + secondaryCollider.Add(new XAttribute("width", colliderHeight * 0.75f)); + } + } + colliderElements.Add(secondaryCollider); + } + var mainElement = new XElement("Ragdoll", + new XAttribute("type", Name), + new XAttribute("texture", TexturePath), + new XAttribute("canentersubmarine", CanEnterSubmarine), + colliderElements, + LimbXElements.Values, + JointXElements); + Wizard.Instance.CreateCharacter(mainElement); + return true; + }; + return box; + } + + private void CreateLimbGUIElement(RectTransform parent, int elementSize, int id, string name = "", LimbType limbType = LimbType.None, Rectangle? sourceRect = null) + { + var limbElement = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementSize * 5 + 40), parent), style: null, color: Color.Gray * 0.25f) + { + CanBeFocused = false + }; + var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 2 }; + var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), name); + 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); + 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) + { + MinValueInt = 0, + MaxValueInt = byte.MaxValue, + IntValue = id, + OnValueChanged = numInput => + { + id = numInput.IntValue; + string text = nameField.GetChild().Text; + string t = string.IsNullOrWhiteSpace(text) ? id.ToString() : text; + label.Text = t; + } + }; + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopLeft), TextManager.Get("Name")); + var nameInput = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopRight), name) + { + CaretColor = Color.White, + }; + nameInput.OnTextChanged += (tb, text) => + { + string t = string.IsNullOrWhiteSpace(text) ? id.ToString() : text; + label.Text = t; + return true; + }; + LimbGUIElements.Add(limbElement); + } + + private void CreateJointGUIElement(RectTransform parent, int elementSize, int id1 = 0, int id2 = 1, Vector2? anchor1 = null, Vector2? anchor2 = null, string jointName = "") + { + var jointElement = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementSize * 6 + 40), parent), style: null, color: Color.Gray * 0.25f) + { + CanBeFocused = false + }; + var group = new GUILayoutGroup(new RectTransform(Vector2.One, jointElement.RectTransform)) { AbsoluteSpacing = 2 }; + var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), jointName); + var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopLeft), TextManager.Get("Name")); + var nameInput = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopRight), jointName) + { + CaretColor = Color.White, + }; + nameInput.OnTextChanged += (textB, text) => + { + jointName = text; + label.Text = jointName; + return true; + }; + var limb1Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")); + var limb1InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + { + MinValueInt = 0, + MaxValueInt = byte.MaxValue, + IntValue = id1 + }; + var limb2Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")); + var limb2InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + { + MinValueInt = 0, + 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); + label.Text = GetJointName(jointName); + limb1InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); + limb2InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); + JointGUIElements.Add(jointElement); + string GetJointName(string n) => string.IsNullOrWhiteSpace(n) ? $"{GetCharacterEditorTranslation("Joint")} {limb1InputField.IntValue} - {limb2InputField.IntValue}" : n; + } + } + + private abstract class View + { + // Easy accessors to the common data. + + public bool IsCopy => Instance.IsCopy; + public IEnumerable SourceAnimations => Instance.SourceAnimations; + public CharacterParams SourceCharacter => Instance.SourceCharacter; + public RagdollParams SourceRagdoll => Instance.SourceRagdoll; + + public string Name + { + get => Instance.name; + set => Instance.name = value; + } + public bool IsHumanoid + { + get => Instance.isHumanoid; + set => Instance.isHumanoid = value; + } + public bool CanEnterSubmarine + { + get => Instance.canEnterSubmarine; + set => Instance.canEnterSubmarine = value; + } + public ContentPackage ContentPackage + { + get => Instance.contentPackage; + set => Instance.contentPackage = value; + } + public string TexturePath + { + get => Instance.texturePath; + set => Instance.texturePath = value; + } + public string XMLPath + { + get => Instance.xmlPath; + set => Instance.xmlPath = value; + } + public Dictionary LimbXElements + { + get => Instance.limbXElements; + set => Instance.limbXElements = value; + } + public List LimbGUIElements + { + get => Instance.limbGUIElements; + set => Instance.limbGUIElements = value; + } + public List JointXElements + { + get => Instance.jointXElements; + set => Instance.jointXElements = value; + } + public List JointGUIElements + { + get => Instance.jointGUIElements; + set => Instance.jointGUIElements = value; + } + + private GUIMessageBox box; + public GUIMessageBox Box + { + get + { + if (box == null) + { + box = Create(); + } + return box; + } + } + + protected abstract GUIMessageBox Create(); + protected static T Get(ref T instance) where T : View, new() + { + if (instance == null) + { + instance = new T(); + } + return instance; + } + + public abstract void Release(); + + protected void ParseLimbsFromGUIElements() + { + LimbXElements.Clear(); + for (int i = 0; i < LimbGUIElements.Count; i++) + { + var limbGUIElement = LimbGUIElements[i]; + var allChildren = limbGUIElement.GetAllChildren(); + GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; + int id = GetField(GetCharacterEditorTranslation("ID")).Parent.GetChild().IntValue; + string limbName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; + LimbType limbType = (LimbType)GetField(GetCharacterEditorTranslation("LimbType")).Parent.GetChild().SelectedData; + // Reverse, because the elements are created from right to left + var rectInputs = GetField(GetCharacterEditorTranslation("SourceRectangle")).Parent.GetAllChildren().Where(c => c is GUINumberInput).Select(c => c as GUINumberInput).Reverse().ToArray(); + int width = rectInputs[2].IntValue; + int height = rectInputs[3].IntValue; + var colliderAttributes = new List(); + // Capsules/Circles + //if (width == height) + //{ + // colliderAttributes.Add(new XAttribute("radius", (int)(width / 2 * 0.85f))); + //} + //else + //{ + // if (height > width) + // { + // colliderAttributes.Add(new XAttribute("radius", (int)(width / 2 * 0.85f))); + // colliderAttributes.Add(new XAttribute("height",(int) (height - width * 0.85f))); + // } + // else + // { + // colliderAttributes.Add(new XAttribute("radius", (int)(height / 2 * 0.85f))); + // colliderAttributes.Add(new XAttribute("width", (int)(width - height * 0.85f))); + // } + //} + // Rectangles + colliderAttributes.Add(new XAttribute("height", (int)(height * 0.85f))); + colliderAttributes.Add(new XAttribute("width", (int)(width * 0.85f))); + idToCodeName.TryGetValue(id, out string notes); + LimbXElements.Add(id.ToString(), new XElement("limb", + new XAttribute("id", id), + new XAttribute("name", limbName), + new XAttribute("type", limbType.ToString()), + colliderAttributes, + new XElement("sprite", + new XAttribute("texture", ""), + new XAttribute("sourcerect", $"{rectInputs[0].IntValue}, {rectInputs[1].IntValue}, {width}, {height}")), + new XAttribute("notes", null ?? string.Empty) + )); + } + } + + protected void ParseJointsFromGUIElements() + { + JointXElements.Clear(); + for (int i = 0; i < JointGUIElements.Count; i++) + { + var jointGUIElement = JointGUIElements[i]; + var allChildren = jointGUIElement.GetAllChildren(); + GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; + string jointName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; + int limb1ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")).Parent.GetChild().IntValue; + int limb2ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")).Parent.GetChild().IntValue; + // Reverse, because the elements are created from right to left + var anchor1Inputs = GetField(GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "1")).Parent.GetAllChildren().Where(c => c is GUINumberInput).Select(c => c as GUINumberInput).Reverse().ToArray(); + var anchor2Inputs = GetField(GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "2")).Parent.GetAllChildren().Where(c => c is GUINumberInput).Select(c => c as GUINumberInput).Reverse().ToArray(); + JointXElements.Add(new XElement("joint", + new XAttribute("name", jointName), + new XAttribute("limb1", limb1ID), + new XAttribute("limb2", limb2ID), + new XAttribute("limb1anchor", $"{anchor1Inputs[0].FloatValue.Format(2)}, {anchor1Inputs[1].FloatValue.Format(2)}"), + new XAttribute("limb2anchor", $"{anchor2Inputs[0].FloatValue.Format(2)}, {anchor2Inputs[1].FloatValue.Format(2)}"))); + } + } + + Dictionary idToCodeName = new Dictionary(); + protected void ParseRagdollFromHTML(string path, Action limbCallback = null, Action jointCallback = null) + { + // TODO: parse as xml files -> allows to load ragdolls onto the wizard. + //XDocument doc = XMLExtensions.TryLoadXml(path); + //var xElements = doc.Elements().ToArray(); + string html = string.Empty; + try + { + html = File.ReadAllText(path); + } + catch (Exception e) + { + DebugConsole.ThrowError(GetCharacterEditorTranslation("FailedToReadHTML").Replace("[path]", path), e); + return; + } + + var lines = html.Split(new string[] { "", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Where(s => s.Contains("left") && s.Contains("top") && s.Contains("width") && s.Contains("height")); + int id = 0; + Dictionary hierarchyToID = new Dictionary(); + Dictionary idToHierarchy = new Dictionary(); + Dictionary idToPositionCode = new Dictionary(); + Dictionary idToName = new Dictionary(); + idToCodeName.Clear(); + foreach (var line in lines) + { + var codeNames = new string(line.SkipWhile(c => c != '>').Skip(1).ToArray()).Split(','); + for (int i = 0; i < codeNames.Length; i++) + { + string codeName = codeNames[i].Trim(); + if (string.IsNullOrWhiteSpace(codeName)) { continue; } + idToCodeName.Add(id, codeName); + string limbName = new string(codeName.SkipWhile(c => c != '_').Skip(1).ToArray()); + if (string.IsNullOrWhiteSpace(limbName)) { continue; } + idToName.Add(id, limbName); + var parts = line.Split(' '); + int ParseToInt(string selector) + { + string part = parts.First(p => p.Contains(selector)); + string s = new string(part.SkipWhile(c => c != ':').Skip(1).TakeWhile(c => char.IsNumber(c)).ToArray()); + int.TryParse(s, out int v); + return v; + }; + // example: 111311cr -> 111311 + string hierarchy = new string(codeName.TakeWhile(c => char.IsNumber(c)).ToArray()); + if (hierarchyToID.ContainsKey(hierarchy)) + { + DebugConsole.ThrowError(GetCharacterEditorTranslation("MultipleItemsWithSameHierarchy").Replace("[hierarchy]", hierarchy).Replace("[name]", codeName)); + return; + } + hierarchyToID.Add(hierarchy, id); + idToHierarchy.Add(id, hierarchy); + string positionCode = new string(codeName.SkipWhile(c => char.IsNumber(c)).TakeWhile(c => c != '_').ToArray()); + idToPositionCode.Add(id, positionCode.ToLowerInvariant()); + int x = ParseToInt("left"); + int y = ParseToInt("top"); + int width = ParseToInt("width"); + int height = ParseToInt("height"); + // This is overridden when the data is loaded from the gui fields. + LimbXElements.Add(hierarchy, new XElement("limb", + new XAttribute("id", id), + new XAttribute("name", limbName), + new XAttribute("type", ParseLimbType(limbName).ToString()), + new XElement("sprite", + new XAttribute("texture", ""), + new XAttribute("sourcerect", $"{x}, {y}, {width}, {height}")) + )); + limbCallback?.Invoke(id, limbName, ParseLimbType(limbName), new Rectangle(x, y, width, height)); + id++; + } + } + for (int i = 0; i < id; i++) + { + if (idToHierarchy.TryGetValue(i, out string hierarchy)) + { + if (hierarchy != "0") + { + // NEW LOGIC: if hierarchy length == 1, parent to 0 + // Else parent to the last bone in the current hierarchy (11 is parented to 1, 212 is parented to 21 etc) + string parent = hierarchy.Length > 1 ? hierarchy.Remove(hierarchy.Length - 1, 1) : "0"; + if (hierarchyToID.TryGetValue(parent, out int parentID)) + { + Vector2 anchor1 = Vector2.Zero; + Vector2 anchor2 = Vector2.Zero; + idToName.TryGetValue(parentID, out string parentName); + idToName.TryGetValue(i, out string limbName); + string jointName = $"{GetCharacterEditorTranslation("Joint")} {parentName} - {limbName}"; + if (idToPositionCode.TryGetValue(i, out string positionCode)) + { + float scalar = 0.8f; + if (LimbXElements.TryGetValue(parent, out XElement parentElement)) + { + Rectangle parentSourceRect = parentElement.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); + float parentWidth = parentSourceRect.Width / 2 * scalar; + float parentHeight = parentSourceRect.Height / 2 * scalar; + switch (positionCode) + { + case "tl": // -1, 1 + anchor1 = new Vector2(-parentWidth, parentHeight); + break; + case "tc": // 0, 1 + anchor1 = new Vector2(0, parentHeight); + break; + case "tr": // -1, 1 + anchor1 = new Vector2(-parentWidth, parentHeight); + break; + case "cl": // -1, 0 + anchor1 = new Vector2(-parentWidth, 0); + break; + case "cr": // 1, 0 + anchor1 = new Vector2(parentWidth, 0); + break; + case "bl": // -1, -1 + anchor1 = new Vector2(-parentWidth, -parentHeight); + break; + case "bc": // 0, -1 + anchor1 = new Vector2(0, -parentHeight); + break; + case "br": // 1, -1 + anchor1 = new Vector2(parentWidth, -parentHeight); + break; + } + if (LimbXElements.TryGetValue(hierarchy, out XElement element)) + { + Rectangle sourceRect = element.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); + float width = sourceRect.Width / 2 * scalar; + float height = sourceRect.Height / 2 * scalar; + switch (positionCode) + { + // Inverse + case "tl": + // br + anchor2 = new Vector2(-width, -height); + break; + case "tc": + // bc + anchor2 = new Vector2(0, -height); + break; + case "tr": + // bl + anchor2 = new Vector2(-width, -height); + break; + case "cl": + // cr + anchor2 = new Vector2(width, 0); + break; + case "cr": + // cl + anchor2 = new Vector2(-width, 0); + break; + case "bl": + // tr + anchor2 = new Vector2(-width, height); + break; + case "bc": + // tc + anchor2 = new Vector2(0, height); + break; + case "br": + // tl + anchor2 = new Vector2(-width, height); + break; + } + } + } + } + // This is overridden when the data is loaded from the gui fields. + JointXElements.Add(new XElement("joint", + new XAttribute("name", jointName), + new XAttribute("limb1", parentID), + new XAttribute("limb2", i), + new XAttribute("limb1anchor", $"{anchor1.X.Format(2)}, {anchor1.Y.Format(2)}"), + new XAttribute("limb2anchor", $"{anchor2.X.Format(2)}, {anchor2.Y.Format(2)}") + )); + jointCallback?.Invoke(parentID, i, anchor1, anchor2, jointName); + } + } + } + } + } + + protected LimbType ParseLimbType(string limbName) + { + var limbType = LimbType.None; + string n = limbName.ToLowerInvariant(); + switch (n) + { + case "head": + limbType = LimbType.Head; + break; + case "torso": + limbType = LimbType.Torso; + break; + case "waist": + case "pelvis": + limbType = LimbType.Waist; + break; + case "tail": + limbType = LimbType.Tail; + break; + } + if (limbType == LimbType.None) + { + if (n.Contains("tail")) + { + limbType = LimbType.Tail; + } + else if (n.Contains("arm") && !n.Contains("lower")) + { + if (n.Contains("right")) + { + limbType = LimbType.RightArm; + } + else if (n.Contains("left")) + { + limbType = LimbType.LeftArm; + } + } + else if (n.Contains("hand") || n.Contains("palm")) + { + if (n.Contains("right")) + { + limbType = LimbType.RightHand; + } + else if (n.Contains("left")) + { + limbType = LimbType.LeftHand; + } + } + else if (n.Contains("thigh") || n.Contains("upperleg")) + { + if (n.Contains("right")) + { + limbType = LimbType.RightThigh; + } + else if (n.Contains("left")) + { + limbType = LimbType.LeftThigh; + } + } + else if (n.Contains("shin") || n.Contains("lowerleg")) + { + if (n.Contains("right")) + { + limbType = LimbType.RightLeg; + } + else if (n.Contains("left")) + { + limbType = LimbType.LeftLeg; + } + } + else if (n.Contains("foot")) + { + if (n.Contains("right")) + { + limbType = LimbType.RightFoot; + } + else if (n.Contains("left")) + { + limbType = LimbType.LeftFoot; + } + } + } + return limbType; + } + + public static bool IsValid(IEnumerable elements, bool isHumanoid, out string missingType) + { + missingType = "none"; + if (!HasAtLeastOneLimbOfType(elements, "torso") && !HasAtLeastOneLimbOfType(elements, "head")) + { + missingType = "TorsoOrHead"; + return false; + } + if (isHumanoid) + { + if (!HasOnlyOneLimbOfType(elements, missingType = "LeftArm")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "LeftHand")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "RightArm")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "RightHand")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "Waist")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "LeftThigh")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "LeftLeg")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "LeftFoot")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "RightThigh")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "RightLeg")) { return false; } + if (!HasOnlyOneLimbOfType(elements, missingType = "RightFoot")) { return false; } + } + return true; + } + + public static bool HasAtLeastOneLimbOfType(IEnumerable elements, string type) => elements.Any(e => IsType(e, type)); + public static bool HasOnlyOneLimbOfType(IEnumerable elements, string type) => elements.Count(e => IsType(e, type)) == 1; + private static bool IsType(XElement element, string type) => element.GetAttributeString("type", "").Equals(type, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs index 594fac1ca..588220be6 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs @@ -17,6 +17,7 @@ namespace Barotrauma GameMain.Instance.OnResolutionChanged += () => { ClearChildren(); Load(); }; var doc = XMLExtensions.TryLoadXml(configFile); + if (doc == null) { return; } configElement = doc.Root; Load(); @@ -34,126 +35,17 @@ namespace Barotrauma foreach (XElement subElement in configElement.Elements()) { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "text": - AddTextElement(subElement, listBox.Content.RectTransform); - break; - case "gridtext": - AddGridTextElement(subElement, listBox.Content.RectTransform); - break; - case "spacing": - AddSpacingElement(subElement, listBox.Content.RectTransform); - break; - case "image": - AddImageElement(subElement, listBox.Content.RectTransform); - break; - } + GUIComponent.FromXML(subElement, listBox.Content.RectTransform); } + foreach (GUIComponent child in listBox.Children) + { + child.CanBeFocused = false; + } + + listBox.RecalculateChildren(); listBox.UpdateScrollBarSize(); } - - private GUIComponent AddTextElement(XElement element, RectTransform parent, string overrideText = null, Anchor anchor = Anchor.Center) - { - var text = overrideText ?? element.ElementInnerText().Replace(@"\n", "\n"); - Color color = element.GetAttributeColor("color", Color.White); - float scale = element.GetAttributeFloat("scale", 1.0f); - Alignment alignment = Alignment.Center; - Enum.TryParse(element.GetAttributeString("alignment", "Center"), out alignment); - ScalableFont font = GUI.Font; - switch (element.GetAttributeString("font", "Font").ToLowerInvariant()) - { - case "font": - font = GUI.Font; - break; - case "smallfont": - font = GUI.SmallFont; - break; - case "largefont": - font = GUI.LargeFont; - break; - case "videotitlefont": - font = GUI.VideoTitleFont; - break; - case "objectivetitlefont": - font = GUI.ObjectiveTitleFont; - break; - case "objectivenamefont": - font = GUI.ObjectiveNameFont; - break; - } - - var textHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), parent), style: null); - var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), textHolder.RectTransform, anchor), - text, - color, - font, - alignment, - wrap: true) - { - TextScale = scale - }; - textBlock.RectTransform.IsFixedSize = textHolder.RectTransform.IsFixedSize = true; - textBlock.RectTransform.NonScaledSize = new Point(textBlock.Rect.Width, textBlock.Rect.Height); - textHolder.RectTransform.NonScaledSize = new Point(textHolder.Rect.Width, textBlock.Rect.Height); - return textHolder; - } - - private void AddGridTextElement(XElement element, RectTransform parent) - { - var text = element.ElementInnerText().Replace(@"\n", "\n"); - string[] elements = text.Split(','); - RectTransform lineContainer = null; - for (int i = 0; i < elements.Length; i++) - { - switch (i % 3) - { - case 0: - lineContainer = AddTextElement(element, parent, elements[i], Anchor.CenterLeft).RectTransform; - lineContainer.Anchor = Anchor.TopCenter; - lineContainer.Pivot = Pivot.TopCenter; - lineContainer.NonScaledSize = new Point((int)(parent.NonScaledSize.X * 0.7f), lineContainer.NonScaledSize.Y); - break; - case 1: - AddTextElement(element, lineContainer, elements[i], Anchor.Center).GetChild().TextAlignment = Alignment.Center; - break; - case 2: - AddTextElement(element, lineContainer, elements[i], Anchor.CenterRight).GetChild().TextAlignment = Alignment.CenterRight; - break; - } - } - } - - private void AddSpacingElement(XElement element, RectTransform parent) - { - if (element.Attribute("absoluteheight") != null) - { - int absoluteHeight = element.GetAttributeInt("absoluteheight", 10); - var textHolder = new GUIFrame(new RectTransform(new Point(parent.NonScaledSize.X, absoluteHeight), parent), style: null); - } - else - { - float relativeHeight = element.GetAttributeFloat("relativeheight", 0.0f); - var textHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, relativeHeight), parent), style: null); - } - } - - private void AddImageElement(XElement element, RectTransform parent) - { - Sprite sprite = new Sprite(element); - - if (element.Attribute("absoluteheight") != null) - { - int absoluteHeight = element.GetAttributeInt("absoluteheight", 10); - new GUIImage(new RectTransform(new Point(parent.NonScaledSize.X, absoluteHeight), parent), sprite, scaleToFit: true); - } - else - { - float relativeHeight = element.GetAttributeFloat("relativeheight", 0.0f); - new GUIImage(new RectTransform(new Vector2(1.0f, relativeHeight), parent), sprite, scaleToFit: true); - } - } - + public void Restart() { listBox.BarScroll = 0.0f; diff --git a/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs index cf8126a46..bac9172fd 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs @@ -501,7 +501,7 @@ namespace Barotrauma foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) { @@ -523,7 +523,7 @@ namespace Barotrauma foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.List) { @@ -549,7 +549,7 @@ namespace Barotrauma foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } bool elementFound = false; foreach (XElement element in doc.Root.Elements()) @@ -664,7 +664,7 @@ namespace Barotrauma foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc?.Root == null) continue; + if (doc == null) { continue; } var newElement = new XElement(newPrefab.Name); newPrefab.Save(newElement); newElement.Add(new XElement("Sprite", diff --git a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs index 01651d775..9e03d9234 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs @@ -4,10 +4,12 @@ using Barotrauma.Tutorials; using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using RestSharp; using System; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net; using System.Threading; using System.Xml.Linq; @@ -68,6 +70,16 @@ namespace Barotrauma RelativeSpacing = 0.02f }; + FetchRemoteContent(Frame.RectTransform); + /*var doc = XMLExtensions.TryLoadXml("Content/UI/MenuTextTest.xml"); + if (doc?.Root != null) + { + foreach (XElement subElement in doc?.Root.Elements()) + { + GUIComponent.FromXML(subElement, Frame.RectTransform); + } + }*/ + // === CAMPAIGN var campaignHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1.0f), parent: buttonsParent.RectTransform) { RelativeOffset = new Vector2(0.1f, 0.0f) }, isHorizontal: true); @@ -360,6 +372,7 @@ namespace Barotrauma { OnClicked = SelectTab }; + } #endregion @@ -506,7 +519,8 @@ namespace Barotrauma } } else - { + { + titleText.Visible = true; selectedTab = 0; } @@ -575,7 +589,7 @@ namespace Barotrauma //(gamesession.GameMode as SinglePlayerCampaign).GenerateMap(ToolBox.RandomSeed(8)); gamesession.StartRound(ToolBox.RandomSeed(8)); GameMain.GameScreen.Select(); - + // TODO: modding support string[] jobIdentifiers = new string[] { "captain", "engineer", "mechanic" }; for (int i = 0; i < 3; i++) { @@ -587,14 +601,14 @@ namespace Barotrauma return; } var characterInfo = new CharacterInfo( - Character.HumanConfigFile, - jobPrefab: JobPrefab.List.Find(j => j.Identifier == jobIdentifiers[i])); + Character.HumanSpeciesName, + jobPrefab: JobPrefab.Get(jobIdentifiers[i])); if (characterInfo.Job == null) { DebugConsole.ThrowError("Failed to find the job \"" + jobIdentifiers[i] + "\"!"); } - var newCharacter = Character.Create(Character.HumanConfigFile, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), characterInfo); + var newCharacter = Character.Create(Character.HumanSpeciesName, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), characterInfo); newCharacter.GiveJobItems(spawnPoint); gamesession.CrewManager.AddCharacter(newCharacter); Character.Controlled = newCharacter; @@ -872,7 +886,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); if (mouseOn && PlayerInput.LeftButtonClicked()) { - Process.Start("http://privacypolicy.daedalic.com"); + GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); } } textPos.Y -= textSize.Y; @@ -955,7 +969,7 @@ namespace Barotrauma if (File.Exists(ServerSettings.SettingsFile)) { XDocument settingsDoc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); - if (settingsDoc?.Root != null) + if (settingsDoc != null) { port = settingsDoc.Root.GetAttributeInt("port", port); queryPort = settingsDoc.Root.GetAttributeInt("queryport", queryPort); @@ -1050,7 +1064,47 @@ namespace Barotrauma OnClicked = HostServerClicked }; } -#endregion + #endregion + private void FetchRemoteContent(RectTransform parent) + { + if (string.IsNullOrEmpty(GameMain.Config.RemoteContentUrl)) { return; } + try + { + var client = new RestClient(GameMain.Config.RemoteContentUrl); + var request = new RestRequest("MenuContent.xml", Method.GET); + + IRestResponse response = client.Execute(request); + if (response.ResponseStatus != ResponseStatus.Completed) + { + return; + } + if (response.StatusCode != HttpStatusCode.OK) + { + return; + } + + string xml = response.Content; + int index = xml.IndexOf('<'); + if (index > 0) { xml = xml.Substring(index, xml.Length - index); } + if (string.IsNullOrWhiteSpace(xml)) { return; } + + XElement element = XDocument.Parse(xml)?.Root; + foreach (XElement subElement in element.Elements()) + { + GUIComponent.FromXML(subElement, parent); + } + } + + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("Fetching remote content to the main menu failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.FetchRemoteContent:Exception", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Fetching remote content to the main menu failed. " + e.Message); + return; + } + } } } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs index 5abe026f0..bc8f6711e 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs @@ -449,11 +449,7 @@ namespace Barotrauma { UserData = mode }; - //TODO: translate mission descriptions - if (TextManager.Language == "English") - { - textBlock.ToolTip = mode.Description; - } + textBlock.ToolTip = mode.Description; } //mission type ------------------------------------------------------------------ @@ -731,7 +727,7 @@ namespace Barotrauma ReadyToStartBox.Selected = false; if (campaignUI != null) { - //SelectTab(Tab.Map); + campaignUI.SelectTab(CampaignUI.Tab.Map); if (campaignUI.StartButton != null) { campaignUI.StartButton.Visible = !GameMain.Client.GameStarted && @@ -909,7 +905,7 @@ namespace Barotrauma { if (characterInfo == null) { - characterInfo = new CharacterInfo(Character.HumanConfigFile, GameMain.NetworkMember.Name, null); + characterInfo = new CharacterInfo(Character.HumanSpeciesName, GameMain.Client.Name, null); characterInfo.RecreateHead( GameMain.Config.CharacterHeadIndex, GameMain.Config.CharacterRace, @@ -930,7 +926,7 @@ namespace Barotrauma UserData = characterInfo }; - CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), infoContainer.RectTransform), characterInfo.Name, font: GUI.LargeFont, textAlignment: Alignment.Center) + CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), infoContainer.RectTransform), characterInfo.Name, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, OverflowClip = true @@ -947,7 +943,7 @@ namespace Barotrauma else { ReadyToStartBox.Selected = false; - GameMain.Client.Name = tb.Text; + GameMain.Client.SetName(tb.Text); }; }; @@ -1011,7 +1007,7 @@ namespace Barotrauma int i = 1; foreach (string jobIdentifier in GameMain.Config.JobPreferences) { - JobPrefab job = JobPrefab.List.Find(j => j.Identifier == jobIdentifier); + if (!JobPrefab.List.TryGetValue(jobIdentifier, out JobPrefab job)) { continue; } if (job == null || job.MaxNumber <= 0) continue; var jobFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), jobList.Content.RectTransform) { MinSize = new Point(0, 20) }, style: "ListBoxElement") @@ -1247,6 +1243,12 @@ namespace Barotrauma }; } + if (!sub.RequiredContentPackagesInstalled) + { + subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); + frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.ToolTip; + } + if (sub.HasTag(SubmarineTag.Shuttle)) { new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.1f, 0.0f) }, @@ -1270,14 +1272,32 @@ namespace Barotrauma public bool VotableClicked(GUIComponent component, object userData) { - if (GameMain.Client == null) return false; + if (GameMain.Client == null) { return false; } VoteType voteType; if (component.Parent == GameMain.NetLobbyScreen.SubList.Content) { if (!GameMain.Client.ServerSettings.Voting.AllowSubVoting) { - if (GameMain.Client.HasPermission(ClientPermissions.SelectSub)) + var selectedSub = component.UserData as Submarine; + if (!selectedSub.RequiredContentPackagesInstalled) + { + var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), + selectedSub.RequiredContentPackages.Any() ? + TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)) : + TextManager.Get("ContentPackageMismatchWarningGeneric"), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = msgBox.Close; + msgBox.Buttons[0].OnClicked += (button, obj) => + { + GameMain.Client.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: false); + return true; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + return false; + } + else if (GameMain.Client.HasPermission(ClientPermissions.SelectSub)) { GameMain.Client.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: false); return true; @@ -1714,16 +1734,6 @@ namespace Barotrauma jobInfoFrame?.AddToGUIUpdateList(); } - public List GetSubList() - { - List subs = new List(); - foreach (GUIComponent component in subList.Content.Children) - { - if (component.UserData is Submarine) subs.Add((Submarine)component.UserData); - } - - return subs; - } public override void Update(double deltaTime) { @@ -2057,7 +2067,7 @@ namespace Barotrauma .UserData as Submarine; //matching sub found and already selected, all good - if (sub != null && subList.SelectedData is Submarine selectedSub && selectedSub.MD5Hash?.Hash == md5Hash) + if (sub != null && subList.SelectedData is Submarine selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) { return true; } @@ -2090,12 +2100,12 @@ namespace Barotrauma FailedSelectedShuttle = null; //hashes match, all good - if (sub.MD5Hash?.Hash == md5Hash) + if (sub.MD5Hash?.Hash == md5Hash && Submarine.SavedSubmarines.Contains(sub)) { return true; } } - + //------------------------------------------------------------------------------------- //if we get to this point, a matching sub was not found or it has an incorrect MD5 hash @@ -2105,14 +2115,15 @@ namespace Barotrauma FailedSelectedShuttle = new Pair(subName, md5Hash); string errorMsg = ""; - if (sub == null) + if (sub == null || !Submarine.SavedSubmarines.Contains(sub)) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", subName) + " "; } else if (sub.MD5Hash?.Hash == null) { errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", subName) + " "; - subList.Content.GetChildByUserData(sub).GetChild().TextColor = Color.Red; + GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); + if (textBlock != null) { textBlock.TextColor = Color.Red; } } else { diff --git a/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs index be7fed2b2..4248b1f50 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs @@ -186,7 +186,7 @@ namespace Barotrauma foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } var prefabList = GameMain.ParticleManager.GetPrefabList(); foreach (ParticlePrefab prefab in prefabList) diff --git a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs index 679665b32..d6b23ad2e 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs @@ -85,7 +85,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName")); clientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") { - Text = GameMain.Config.DefaultPlayerName, + Text = GameMain.Config.PlayerName, MaxTextLength = Client.MaxNameLength, OverflowClip = true }; @@ -822,11 +822,20 @@ namespace Barotrauma private void ServerQueryFinished() { - if (serverList.Content.Children.All(c => !c.Visible)) + if (!serverList.Content.Children.Any()) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), serverList.Content.RectTransform), - TextManager.Get("NoMatchingServers")) + new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), + TextManager.Get("NoServers"), textAlignment: Alignment.Center) { + CanBeFocused = false + }; + } + else if (serverList.Content.Children.All(c => !c.Visible)) + { + new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), + TextManager.Get("NoMatchingServers"), textAlignment: Alignment.Center) + { + CanBeFocused = false, UserData = "noresults" }; } @@ -918,7 +927,7 @@ namespace Barotrauma return false; } - GameMain.Config.DefaultPlayerName = clientNameBox.Text; + GameMain.Config.PlayerName = clientNameBox.Text; GameMain.Config.SaveNewPlayerConfig(); CoroutineManager.StartCoroutine(ConnectToServer(ip, serverName)); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs index fd5456641..8cbafe767 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs @@ -301,7 +301,7 @@ namespace Barotrauma if (file.Path.EndsWith(".xml")) { XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc != null && doc.Root != null) + if (doc != null) { LoadSprites(doc.Root); } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs index b99db8748..f095bc0c9 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs @@ -164,8 +164,7 @@ namespace Barotrauma if (userdata is Facepunch.Steamworks.Workshop.Item item) { if (!item.Installed) { return false; } - CreateWorkshopItem(item); - ShowCreateItemFrame(); + if (CreateWorkshopItem(item)) { ShowCreateItemFrame(); } } return true; } @@ -310,7 +309,7 @@ namespace Barotrauma CreateMyItemFrame(contentPackage, myItemList); } } - + private void OnItemsReceived(IList itemDetails, GUIListBox listBox) { listBox.ClearChildren(); @@ -525,6 +524,9 @@ namespace Barotrauma OnClicked = DownloadItem }; } + + innerFrame.Recalculate(); + listBox.RecalculateChildren(); } private void RemoveItemFromLists(ulong itemID) @@ -651,6 +653,9 @@ namespace Barotrauma { if (!(tickBox.UserData is Facepunch.Steamworks.Workshop.Item item)) { return false; } + //currently editing the item, don't allow enabling/disabling it + if (itemEditor?.Id == item.Id) { tickBox.Selected = true; return false; } + var updateButton = tickBox.Parent.FindChild("updatebutton"); string errorMsg = ""; @@ -724,23 +729,30 @@ namespace Barotrauma } }; - var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)) { Color = Color.Black }; + var centerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f, + Color = Color.Black * 0.9f + }; if (itemPreviewSprites.ContainsKey(item.PreviewImageUrl)) { - new GUIImage(new RectTransform(Vector2.One, headerArea.RectTransform), itemPreviewSprites[item.PreviewImageUrl], scaleToFit: true); + new GUIImage(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform), itemPreviewSprites[item.PreviewImageUrl], scaleToFit: true); } else { - new GUIImage(new RectTransform(Vector2.One, headerArea.RectTransform), SteamManager.Instance.DefaultPreviewImage, scaleToFit: true); + new GUIImage(new RectTransform(new Vector2(0.5f, 0.0f), centerArea.RectTransform), SteamManager.Instance.DefaultPreviewImage, scaleToFit: true); } - var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform)) { ScrollBarVisible = true }; + var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform)) { ScrollBarVisible = true }; //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform) { MinSize = new Point(0, 5) }, style: null); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform), TextManager.EnsureUTF8(item.Description), wrap: true) + string description = TextManager.EnsureUTF8(item.Description); + description = ToolBox.RemoveBBCodeTags(description); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform), description, wrap: true) { CanBeFocused = false }; @@ -799,12 +811,24 @@ namespace Barotrauma var modificationDate = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), content.RectTransform), TextManager.Get("WorkshopItemModificationDate")); new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), modificationDate.RectTransform, Anchor.CenterRight), item.Modified.ToString("dd.MM.yyyy"), textAlignment: Alignment.TopRight); - } - /*private void CreateWorkshopItem() - { - SteamManager.CreateWorkshopItemStaging("ModName", out itemEditor, out itemContentPackage); - }*/ + if (item.Subscribed) + { + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform) { MinSize = new Point(0, 25) }, isHorizontal: true); + new GUIButton(new RectTransform(new Vector2(0.5f, 0.95f), buttonContainer.RectTransform), TextManager.Get("WorkshopItemUnsubscribe")) + { + UserData = item, + OnClicked = (btn, userdata) => + { + item.UnSubscribe(); + subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); + itemPreviewFrame.ClearChildren(); + return true; + } + }; + } + } + private void CreateWorkshopItem(Submarine sub) { string destinationFolder = Path.Combine("Mods", sub.Name); @@ -826,7 +850,7 @@ namespace Barotrauma itemContentPackage.Name = sub.Name; itemContentPackage.Save(itemContentPackage.Path); ContentPackage.List.Add(itemContentPackage); - GameMain.Config.SelectedContentPackages.Add(itemContentPackage); + GameMain.Config.SelectContentPackage(itemContentPackage); itemEditor.Title = sub.Name; itemEditor.Tags.Add("Submarine"); @@ -886,15 +910,21 @@ namespace Barotrauma }*/ } - private void CreateWorkshopItem(Facepunch.Steamworks.Workshop.Item item) + private bool CreateWorkshopItem(Facepunch.Steamworks.Workshop.Item item) { if (!item.Installed) { - new GUIMessageBox(TextManager.Get("Error"), + new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEdit", "[itemname]", TextManager.EnsureUTF8(item.Title))); - return; + return false; } - SteamManager.CreateWorkshopItemStaging(item, out itemEditor, out itemContentPackage); + 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() @@ -1256,6 +1286,14 @@ namespace Barotrauma createItemFileList.Flash(Color.Red); } + if (!itemContentPackage.CheckValidity(out List errorMessages)) + { + new GUIMessageBox( + TextManager.GetWithVariable("workshopitempublishfailed", "[itemname]", itemEditor.Title), + string.Join("\n", errorMessages)); + return false; + } + PublishWorkshopItem(); return true; } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs index 9208bc39c..941cbf389 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs @@ -242,16 +242,16 @@ namespace Barotrauma //empty guiframe as a separator new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), paddedLeftPanel.RectTransform) { AbsoluteOffset = new Point(0, TopPanel.Rect.Height) }, style: null); - var itemCountText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("Items")); - var itemCount = new GUITextBlock(new RectTransform(Vector2.One, itemCountText.RectTransform), "", textAlignment: Alignment.TopRight); + var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("Items")); + var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.TopRight); itemCount.TextGetter = () => { itemCount.TextColor = ToolBox.GradientLerp(Item.ItemList.Count / 5000.0f, Color.LightGreen, Color.Yellow, Color.Red); return Item.ItemList.Count.ToString(); }; - var structureCountText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("Structures")); - var structureCount = new GUITextBlock(new RectTransform(Vector2.One, structureCountText.RectTransform), "", textAlignment: Alignment.TopRight); + var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("Structures")); + var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.TopRight); structureCount.TextGetter = () => { int count = (MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.hullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count); @@ -259,13 +259,43 @@ namespace Barotrauma return count.ToString(); }; - var wallCountText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("Walls")); - var wallCount = new GUITextBlock(new RectTransform(Vector2.One, wallCountText.RectTransform), "", textAlignment: Alignment.TopRight); + var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("Walls")); + var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.TopRight); wallCount.TextGetter = () => { wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, Color.LightGreen, Color.Yellow, Color.Red); return Structure.WallList.Count.ToString(); }; + + var lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("SubEditorLights")); + var lightCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.TopRight); + lightCount.TextGetter = () => + { + int disabledItemLightCount = 0; + foreach (Item item in Item.ItemList) + { + if (item.ParentInventory == null) { continue; } + disabledItemLightCount += item.GetComponents().Count(); + } + int count = GameMain.LightManager.Lights.Count() - disabledItemLightCount; + lightCount.TextColor = ToolBox.GradientLerp(count / 250.0f, Color.LightGreen, Color.Yellow, Color.Red); + return count.ToString(); + }; + var shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights")); + var shadowCastingLightCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.TopRight); + shadowCastingLightCount.TextGetter = () => + { + int disabledItemLightCount = 0; + foreach (Item item in Item.ItemList) + { + if (item.ParentInventory == null) { continue; } + disabledItemLightCount += item.GetComponents().Count(); + } + int count = GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; + shadowCastingLightCount.TextColor = ToolBox.GradientLerp(count / 60.0f, Color.LightGreen, Color.Yellow, Color.Red); + return count.ToString(); + }; + GUITextBlock.AutoScaleAndNormalize(paddedLeftPanel.Children.Where(c => c is GUITextBlock).Cast()); hullVolumeFrame = new GUIFrame(new RectTransform(new Vector2(0.15f, 2.0f), TopPanel.RectTransform, Anchor.BottomLeft, Pivot.TopLeft, minSize: new Point(300, 85)) { AbsoluteOffset = new Point(LeftPanel.Rect.Width, 0) }, "GUIToolTip") { @@ -855,7 +885,7 @@ namespace Barotrauma { if (dummyCharacter != null) RemoveDummyCharacter(); - dummyCharacter = Character.Create(Character.HumanConfigFile, Vector2.Zero, "", hasAi: false); + dummyCharacter = Character.Create(Character.HumanSpeciesName, Vector2.Zero, "", hasAi: false); //make space for the entity menu for (int i = 0; i < dummyCharacter.Inventory.SlotPositions.Length; i++) @@ -1037,8 +1067,6 @@ namespace Barotrauma ChangeSubDescription(textBox, text); return true; }; - descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Description; - submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), leftColumn.RectTransform), isHorizontal: true) { AbsoluteSpacing = 5 }; @@ -1256,7 +1284,10 @@ namespace Barotrauma { OnClicked = SaveSub }; - + paddedSaveFrame.Recalculate(); + leftColumn.Recalculate(); + descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Description; + submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; } @@ -2258,7 +2289,7 @@ namespace Barotrauma me.IsHighlighted = false; } - if (WiringMode && dummyCharacter.SelectedConstruction==null) + if (WiringMode && dummyCharacter.SelectedConstruction == null) { List wires = new List(); foreach (Item item in Item.ItemList) @@ -2269,8 +2300,29 @@ namespace Barotrauma Wire.UpdateEditing(wires); } - if (dummyCharacter.SelectedConstruction==null || dummyCharacter.SelectedConstruction.GetComponent() != null) + if (dummyCharacter.SelectedConstruction == null || + dummyCharacter.SelectedConstruction.GetComponent() != null) { + if (WiringMode && (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) || PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.Right))) + { + Wire equippedWire = + Character.Controlled?.SelectedItems[0]?.GetComponent() ?? + Character.Controlled?.SelectedItems[1]?.GetComponent(); + if (equippedWire != null && equippedWire.GetNodes().Count > 0) + { + Vector2 lastNode = equippedWire.GetNodes().Last(); + if (equippedWire.Item.Submarine != null) + { + lastNode += equippedWire.Item.Submarine.HiddenSubPosition + equippedWire.Item.Submarine.Position; + } + + dummyCharacter.CursorPosition = + Math.Abs(dummyCharacter.CursorPosition.X - lastNode.X) < Math.Abs(dummyCharacter.CursorPosition.Y - lastNode.Y) ? + new Vector2(lastNode.X, dummyCharacter.CursorPosition.Y) : + dummyCharacter.CursorPosition = new Vector2(dummyCharacter.CursorPosition.X, lastNode.Y); + } + } + Vector2 mouseSimPos = FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition); foreach (Limb limb in dummyCharacter.AnimController.Limbs) { @@ -2403,10 +2455,13 @@ namespace Barotrauma Submarine.DrawBack(spriteBatch, editing: true); spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, transformMatrix: cam.Transform); + Submarine.DrawDamageable(spriteBatch, null, editing: true); + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, transformMatrix: cam.Transform); Submarine.DrawFront(spriteBatch, editing: true); - if (!CharacterMode && !WiringMode && GUI.MouseOn == null) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); diff --git a/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs index e7bac5c7c..398d852a4 100644 --- a/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs @@ -253,19 +253,21 @@ namespace Barotrauma } } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24) : base(style, new RectTransform(Vector2.One, parent)) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) + : this(parent, entity, inGame ? SerializableProperty.GetProperties(entity) : SerializableProperty.GetProperties(entity), showName, style, elementHeight, titleFont) + { + } + + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) + : base(style, new RectTransform(Vector2.One, parent)) { this.elementHeight = (int)(elementHeight * GUI.Scale); - List editableProperties = inGame ? - SerializableProperty.GetProperties(entity) : - SerializableProperty.GetProperties(entity); - layoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform)) { AbsoluteSpacing = 2 }; if (showName) { - new GUITextBlock(new RectTransform(new Point(layoutGroup.Rect.Width, this.elementHeight), layoutGroup.RectTransform), entity.Name, font: GUI.Font); + new GUITextBlock(new RectTransform(new Point(layoutGroup.Rect.Width, this.elementHeight), layoutGroup.RectTransform), entity.Name, font: titleFont ?? GUI.Font); } - editableProperties.ForEach(ep => CreateNewField(ep, entity)); + properties.ForEach(ep => CreateNewField(ep, entity)); //scale the size of this component and the layout group to fit the children int contentHeight = ContentHeight; @@ -300,7 +302,7 @@ namespace Barotrauma { displayName = property.Name.FormatCamelCaseWithSpaces(); } - string toolTip = property.GetAttribute().ToolTip; + string toolTip = property.GetAttribute().Description; GUIComponent propertyField = null; if (value is bool) { @@ -383,24 +385,37 @@ namespace Barotrauma { ToolTip = toolTip }; - GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform, - Anchor.TopRight), GUINumberInput.NumberType.Int) - { - ToolTip = toolTip, - Font = GUI.SmallFont - }; var editableAttribute = property.GetAttribute(); - numberInput.MinValueInt = editableAttribute.MinValueInt; - numberInput.MaxValueInt = editableAttribute.MaxValueInt; - numberInput.IntValue = value; - numberInput.OnValueChanged += (numInput) => + GUIComponent field; + if (editableAttribute.ReadOnly) { - if (property.TrySetValue(entity, numInput.IntValue)) + var numberInput = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform, Anchor.TopRight), value.ToString()) { - TrySendNetworkUpdate(entity, property); - } - }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { numberInput }); } + ToolTip = toolTip, + Font = GUI.SmallFont + }; + field = numberInput as GUIComponent; + } + else + { + var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) + { + ToolTip = toolTip, + Font = GUI.SmallFont + }; + numberInput.MinValueInt = editableAttribute.MinValueInt; + numberInput.MaxValueInt = editableAttribute.MaxValueInt; + numberInput.IntValue = value; + numberInput.OnValueChanged += (numInput) => + { + if (property.TrySetValue(entity, numInput.IntValue)) + { + TrySendNetworkUpdate(entity, property); + } + }; + field = numberInput as GUIComponent; + } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { field }); } return frame; } @@ -411,6 +426,7 @@ namespace Barotrauma { ToolTip = toolTip }; + GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Float) { @@ -450,6 +466,7 @@ namespace Barotrauma { enumDropDown.AddItem(enumValue.ToString(), enumValue); } + enumDropDown.SelectItem(value); enumDropDown.OnSelected += (selected, val) => { if (property.TrySetValue(entity, val)) @@ -458,7 +475,6 @@ namespace Barotrauma } return true; }; - enumDropDown.SelectItem(value); if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { enumDropDown }); } return frame; } @@ -506,8 +522,10 @@ namespace Barotrauma { ToolTip = toolTip }; + var editableAttribute = property.GetAttribute(); GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform)) { + Enabled = editableAttribute != null && !editableAttribute.ReadOnly, ToolTip = toolTip, Font = GUI.SmallFont, Text = value, diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs index 7fedeae71..ab9bc1a0c 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs @@ -289,8 +289,11 @@ namespace Barotrauma.Sounds get { if (!IsPlaying) { return 0.0f; } - + uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); + + if (alSource == 0) { return 0.0f; } + if (!IsStream) { int playbackPos; Al.GetSourcei(alSource, Al.SampleOffset, out playbackPos); diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs index 61516ac78..00c5a4300 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs @@ -116,10 +116,15 @@ namespace Barotrauma foreach (string soundFile in soundFiles) { XDocument doc = XMLExtensions.TryLoadXml(soundFile); - if (doc != null && doc.Root != null) + if (doc == null) { continue; } + var mainElement = doc.Root; + if (doc.Root.IsOverride()) { - soundElements.AddRange(doc.Root.Elements()); + mainElement = doc.Root.FirstElement(); + DebugConsole.NewMessage($"Overriding all sounds with {soundFile}", Color.Yellow); + soundElements.Clear(); } + soundElements.AddRange(mainElement.Elements()); } SoundCount = 1 + soundElements.Count(); diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DecorativeSprite.cs new file mode 100644 index 000000000..8b4bd88a1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DecorativeSprite.cs @@ -0,0 +1,142 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using SpriteParams = Barotrauma.RagdollParams.SpriteParams; + +namespace Barotrauma +{ + class DecorativeSprite : ISerializableEntity + { + public string Name => $"Decorative Sprite"; + public Dictionary SerializableProperties { get; set; } + + public Sprite Sprite { get; private set; } + + public enum AnimationType + { + None, + Sine, + Noise + } + + [Serialize("0,0", true), Editable] + public Vector2 Offset { get; private set; } + + [Serialize(AnimationType.None, false), Editable] + public AnimationType OffsetAnim { get; private set; } + + [Serialize(0.0f, true), Editable] + public float OffsetAnimSpeed { get; private set; } + + private float rotationSpeedRadians; + [Serialize(0.0f, true), Editable] + public float RotationSpeed + { + get + { + return MathHelper.ToDegrees(rotationSpeedRadians); + } + private set + { + rotationSpeedRadians = MathHelper.ToRadians(value); + } + } + + [Serialize(0.0f, true), Editable] + public float Rotation { get; private set; } + + [Serialize(AnimationType.None, false), 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)] + public int RandomGroupID { get; private set; } + + /// + /// The sprite is only drawn if these conditions are fulfilled + /// + internal List IsActiveConditionals { get; private set; } = new List(); + /// + /// The sprite is only animated if these conditions are fulfilled + /// + internal List AnimationConditionals { get; private set; } = new List(); + + public DecorativeSprite(XElement element, string path = "", string file = "", bool lazyLoad = false) + { + Sprite = new Sprite(element, path, file, lazyLoad: lazyLoad); + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + foreach (XElement subElement in element.Elements()) + { + List conditionalList = null; + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "conditional": + case "isactiveconditional": + conditionalList = IsActiveConditionals; + break; + case "animationconditional": + conditionalList = AnimationConditionals; + break; + default: + continue; + } + foreach (XAttribute attribute in subElement.Attributes()) + { + if (attribute.Name.ToString().ToLowerInvariant() == "targetitemcomponent") { continue; } + conditionalList.Add(new PropertyConditional(attribute)); + } + } + } + + public Vector2 GetOffset(ref float offsetState) + { + if (OffsetAnimSpeed <= 0.0f) + { + return Offset; + } + switch (OffsetAnim) + { + case AnimationType.Sine: + offsetState = offsetState % (MathHelper.TwoPi / OffsetAnimSpeed); + return Offset * (float)Math.Sin(offsetState * OffsetAnimSpeed); + case AnimationType.Noise: + offsetState = offsetState % (1.0f / (OffsetAnimSpeed * 0.1f)); + + float t = offsetState * 0.1f * OffsetAnimSpeed; + return new Vector2( + Offset.X * (PerlinNoise.GetPerlin(t, t) - 0.5f), + Offset.Y * (PerlinNoise.GetPerlin(t + 0.5f, t + 0.5f) - 0.5f)); + default: + return Offset; + } + } + + public float GetRotation(ref float rotationState) + { + if (rotationSpeedRadians <= 0.0f) + { + return Rotation; + } + switch (OffsetAnim) + { + case AnimationType.Sine: + rotationState = rotationState % (MathHelper.TwoPi / rotationSpeedRadians); + return Rotation * (float)Math.Sin(rotationState * rotationSpeedRadians); + case AnimationType.Noise: + rotationState = rotationState % (1.0f / rotationSpeedRadians); + return Rotation * PerlinNoise.GetPerlin(rotationState * rotationSpeedRadians, rotationState * rotationSpeedRadians); + default: + return rotationState * rotationSpeedRadians; + } + } + + public void Remove() + { + Sprite?.Remove(); + Sprite = null; + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/CustomDeformation.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/CustomDeformation.cs index b652904ff..e75074efb 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/CustomDeformation.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/CustomDeformation.cs @@ -8,13 +8,12 @@ namespace Barotrauma.SpriteDeformations { class CustomDeformationParams : SpriteDeformationParams { - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "How fast the deformation \"oscillates\" back and forth. " + + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f), + Serialize(0.0f, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "The \"strength\" of the deformation.")] + [Serialize(1.0f, true, description: "The \"strength\" of the deformation."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float Amplitude { get; set; } public CustomDeformationParams(XElement element) : base(element) @@ -26,7 +25,7 @@ namespace Barotrauma.SpriteDeformations { private List deformRows = new List(); - private CustomDeformationParams CustomDeformationParams => deformationParams as CustomDeformationParams; + private CustomDeformationParams CustomDeformationParams => Params as CustomDeformationParams; public override float Phase { @@ -116,11 +115,12 @@ namespace Barotrauma.SpriteDeformations multiplier = CustomDeformationParams.Frequency <= 0.0f ? CustomDeformationParams.Amplitude : (float)Math.Sin(phase) * CustomDeformationParams.Amplitude; + multiplier *= Params.Strength; } public override void Update(float deltaTime) { - if (!deformationParams.UseMovementSine) + if (!Params.UseMovementSine) { phase += deltaTime * CustomDeformationParams.Frequency; phase %= MathHelper.TwoPi; diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/Inflate.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/Inflate.cs index 6e86b2c3c..5c998d4e7 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/Inflate.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/Inflate.cs @@ -31,7 +31,7 @@ namespace Barotrauma.SpriteDeformations private Vector2[,] deformation; - private InflateParams InflateParams => deformationParams as InflateParams; + private InflateParams InflateParams => Params as InflateParams; public Inflate(XElement element) : base(element, new InflateParams(element)) { @@ -58,11 +58,12 @@ namespace Barotrauma.SpriteDeformations { deformation = this.deformation; multiplier = InflateParams.Frequency <= 0.0f ? InflateParams.Scale : (float)(Math.Sin(phase) + 1.0f) / 2.0f * InflateParams.Scale; + multiplier *= Params.Strength; } public override void Update(float deltaTime) { - if (!deformationParams.UseMovementSine) + if (!Params.UseMovementSine) { phase += deltaTime * InflateParams.Frequency; phase %= MathHelper.TwoPi; diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/JointBendDeformation.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/JointBendDeformation.cs index 734fb2132..dd6e69770 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/JointBendDeformation.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/JointBendDeformation.cs @@ -21,7 +21,7 @@ namespace Barotrauma.SpriteDeformations public float BendRight { get { return bendRight; } - set { bendRight = MathHelper.Clamp(value, -maxRotation, maxRotation); } + set { bendRight = MathHelper.Clamp(value, -MaxRotationInRadians, MaxRotationInRadians); } } //the pivot point to rotate the right side around public Vector2 BendRightRefPos = new Vector2(1.0f, 0.5f); @@ -30,7 +30,7 @@ namespace Barotrauma.SpriteDeformations public float BendLeft { get { return bendLeft; } - set { bendLeft = MathHelper.Clamp(value, -maxRotation, maxRotation); } + set { bendLeft = MathHelper.Clamp(value, -MaxRotationInRadians, MaxRotationInRadians); } } public Vector2 BendLeftRefPos = new Vector2(0.0f, 0.5f); @@ -38,7 +38,7 @@ namespace Barotrauma.SpriteDeformations public float BendUp { get { return bendUp; } - set { bendUp = MathHelper.Clamp(value, -maxRotation, maxRotation); } + set { bendUp = MathHelper.Clamp(value, -MaxRotationInRadians, MaxRotationInRadians); } } public Vector2 BendUpRefPos = new Vector2(0.5f, 0.0f); @@ -46,18 +46,15 @@ namespace Barotrauma.SpriteDeformations public float BendDown { get { return bendDown; } - set { bendDown = MathHelper.Clamp(value, -maxRotation, maxRotation); } + set { bendDown = MathHelper.Clamp(value, -MaxRotationInRadians, MaxRotationInRadians); } } public Vector2 BendDownRefPos = new Vector2(0.5f, 1.0f); public Vector2 Scale = Vector2.Zero; - private float maxRotation; + private float MaxRotationInRadians => MathHelper.ToRadians(Params.MaxRotation); - public JointBendDeformation(XElement element) : base(element, new JointBendDeformationParams(element)) - { - maxRotation = MathHelper.ToRadians(element == null ? 90.0f : element.GetAttributeFloat("maxrotation", 90.0f)); - } + public JointBendDeformation(XElement element) : base(element, new JointBendDeformationParams(element)) { } protected override void GetDeformation(out Vector2[,] deformation, out float multiplier) { @@ -80,7 +77,7 @@ namespace Barotrauma.SpriteDeformations { float strength = 1.0f - normalizedPos.X;//(1.0f - Math.Max(normalizedPos.X - BendLeftRefPos.X, 0.0f) / (1.0f - BendLeftRefPos.X)); strength = Math.Max((strength - 0.5f) * 2.0f, 0.0f); - Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendLeftRefPos, BendLeft * strength); + Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendLeftRefPos, BendLeft * strength * Params.Strength); Vector2 offset = rotatedP - normalizedPos; offset.X *= Scale.Y / Scale.X; Deformation[x, y] += offset; @@ -89,7 +86,7 @@ namespace Barotrauma.SpriteDeformations { float strength = normalizedPos.X;//(1.0f - Math.Max(BendRightRefPos.X - normalizedPos.X, 0.0f) / (BendRightRefPos.X)); strength = Math.Max((strength - 0.5f) * 2.0f, 0.0f); - Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendRightRefPos, BendRight * strength); + Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendRightRefPos, BendRight * strength * Params.Strength); Vector2 offset = rotatedP - normalizedPos; offset.X *= Scale.Y / Scale.X; Deformation[x, y] += offset; @@ -99,7 +96,7 @@ namespace Barotrauma.SpriteDeformations { float strength = 1.0f - normalizedPos.Y;//(1.0f - Math.Max(normalizedPos.Y - BendUpRefPos.Y, 0.0f) / (1.0f - BendUpRefPos.Y)); strength = Math.Max((strength - 0.5f) * 2.0f, 0.0f); - Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendUpRefPos, BendUp * strength); + Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendUpRefPos, BendUp * strength * Params.Strength); Vector2 offset = rotatedP - normalizedPos; offset.Y *= Scale.X / Scale.Y; Deformation[x, y] += offset; @@ -108,7 +105,7 @@ namespace Barotrauma.SpriteDeformations { float strength = normalizedPos.Y;//(1.0f - Math.Max(BendDownRefPos.Y - normalizedPos.Y, 0.0f) / (BendDownRefPos.Y)); strength = Math.Max((strength - 0.5f) * 2.0f, 0.0f); - Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendDownRefPos, BendDown * strength); + Vector2 rotatedP = RotatePointAroundTarget(normalizedPos, BendDownRefPos, BendDown * strength * Params.Strength); Vector2 offset = rotatedP - normalizedPos; offset.Y *= Scale.X / Scale.Y; Deformation[x, y] += offset; diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/NoiseDeformation.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/NoiseDeformation.cs index ab1ab4f7e..02f037271 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/NoiseDeformation.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/NoiseDeformation.cs @@ -5,16 +5,13 @@ namespace Barotrauma.SpriteDeformations { class NoiseDeformationParams : SpriteDeformationParams { - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, - ToolTip = "The frequency of the noise.")] + [Serialize(0.0f, true, description: "The frequency of the noise."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public override float Frequency { get; set; } - [Serialize(1.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "How much the noise distorts the sprite.")] + [Serialize(1.0f, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "How fast the noise changes.")] + [Serialize(0.0f, true, 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) @@ -24,7 +21,7 @@ namespace Barotrauma.SpriteDeformations class NoiseDeformation : SpriteDeformation { - private NoiseDeformationParams NoiseDeformationParams => deformationParams as NoiseDeformationParams; + private NoiseDeformationParams NoiseDeformationParams => Params as NoiseDeformationParams; private float phase; @@ -53,7 +50,7 @@ namespace Barotrauma.SpriteDeformations protected override void GetDeformation(out Vector2[,] deformation, out float multiplier) { deformation = Deformation; - multiplier = NoiseDeformationParams.Amplitude; + multiplier = NoiseDeformationParams.Amplitude * Params.Strength; } public override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/PositionalDeformation.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/PositionalDeformation.cs index 0dbd38635..a211ca8b8 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/PositionalDeformation.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/PositionalDeformation.cs @@ -10,30 +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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, - ToolTip = "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)] public float Falloff { get; set; } /// /// Maximum stretch per vertex (1 = the size of the sprite) /// - [Serialize(1.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "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)] public float MaxDeformation { get; set; } /// /// How fast the sprite reacts to being stretched /// - [Serialize(1.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "How fast the sprite reacts to being stretched")] + [Serialize(1.0f, true, 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.1f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "How fast the sprite returns back to normal after stretching ends")] + [Serialize(0.1f, true, 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) @@ -52,7 +48,7 @@ namespace Barotrauma.SpriteDeformations public ReactionType Type; - private PositionalDeformationParams positionalDeformationParams => DeformationParams as PositionalDeformationParams; + private PositionalDeformationParams positionalDeformationParams => Params as PositionalDeformationParams; public PositionalDeformation(XElement element) : base(element, new PositionalDeformationParams(element)) { diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/SpriteDeformation.cs index da7364bcc..da7e27603 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformAnimations/SpriteDeformation.cs @@ -14,7 +14,7 @@ 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)] + [Serialize(-1, true), Editable] public int Sync { get; @@ -35,18 +35,24 @@ namespace Barotrauma.SpriteDeformations set; } - public string Name => GetType().Name; + public string Name => $"Deformation ({TypeName})"; - [Serialize(false, true)] + [Serialize(1.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] + public float Strength { get; private set; } + + [Serialize(90f, true), Editable(MinValueFloat = 0, MaxValueFloat = 90)] + public float MaxRotation { get; private set; } + + [Serialize(false, true), Editable] public bool UseMovementSine { get; set; } - [Serialize(false, true)] + [Serialize(false, true), Editable] public bool StopWhenHostIsDead { get; set; } /// /// Only used if UseMovementSine is enabled. Multiplier for Pi. /// - [Serialize(0f, true)] + [Serialize(0f, true), Editable] public float SineOffset { get; set; } public virtual float Frequency { get; set; } = 1; @@ -54,7 +60,7 @@ namespace Barotrauma.SpriteDeformations public Dictionary SerializableProperties { get; - private set; + set; } /// @@ -97,7 +103,7 @@ namespace Barotrauma.SpriteDeformations protected Vector2[,] Deformation { get; private set; } - protected SpriteDeformationParams deformationParams; + public SpriteDeformationParams Params { get; set; } private static readonly string[] deformationTypes = new string[] { "Inflate", "Custom", "Noise", "BendJoint", "ReactToTriggerers" }; public static IEnumerable DeformationTypes @@ -107,19 +113,13 @@ namespace Barotrauma.SpriteDeformations public Point Resolution { - get { return deformationParams.Resolution; } + get { return Params.Resolution; } set { SetResolution(value); } } - public SpriteDeformationParams DeformationParams - { - get { return deformationParams; } - set { deformationParams = value; } - } + public string TypeName => Params.TypeName; - public string TypeName => deformationParams.TypeName; - - public int Sync => deformationParams.Sync; + public int Sync => Params.Sync; public static SpriteDeformation Load(string deformationType, string parentDebugName) { @@ -174,22 +174,22 @@ namespace Barotrauma.SpriteDeformations if (newDeformation != null) { - newDeformation.deformationParams.TypeName = typeName; + newDeformation.Params.TypeName = typeName; } return newDeformation; } protected SpriteDeformation(XElement element, SpriteDeformationParams deformationParams) { - this.deformationParams = deformationParams; + this.Params = deformationParams; SerializableProperty.DeserializeProperties(deformationParams, element); Deformation = new Vector2[deformationParams.Resolution.X, deformationParams.Resolution.Y]; } public void SetResolution(Point resolution) { - deformationParams.Resolution = resolution; - Deformation = new Vector2[deformationParams.Resolution.X, deformationParams.Resolution.Y]; + Params.Resolution = resolution; + Deformation = new Vector2[Params.Resolution.X, Params.Resolution.Y]; } protected abstract void GetDeformation(out Vector2[,] deformation, out float multiplier); @@ -200,10 +200,10 @@ namespace Barotrauma.SpriteDeformations { foreach (SpriteDeformation animation in animations) { - if (animation.deformationParams.Resolution.X != animation.Deformation.GetLength(0) || - animation.deformationParams.Resolution.Y != animation.Deformation.GetLength(1)) + if (animation.Params.Resolution.X != animation.Deformation.GetLength(0) || + animation.Params.Resolution.Y != animation.Deformation.GetLength(1)) { - animation.Deformation = new Vector2[animation.deformationParams.Resolution.X, animation.deformationParams.Resolution.Y]; + animation.Deformation = new Vector2[animation.Params.Resolution.X, animation.Params.Resolution.Y]; } } @@ -224,7 +224,7 @@ namespace Barotrauma.SpriteDeformations { for (int y = 0; y < resolution.Y; y++) { - switch (animation.deformationParams.BlendMode) + switch (animation.Params.BlendMode) { case DeformationBlendMode.Override: deformation[x,y] = animDeformation[x,y] * scale * multiplier; @@ -244,7 +244,7 @@ namespace Barotrauma.SpriteDeformations public virtual void Save(XElement element) { - SerializableProperty.SerializeProperties(deformationParams, element); + SerializableProperty.SerializeProperties(Params, element); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs index 537cdb93b..fa10d1840 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs @@ -267,7 +267,7 @@ namespace Barotrauma Matrix.CreateTranslation(pos); } - public void Draw(Camera cam, Vector3 pos, Vector2 origin, float rotate, Vector2 scale, Color color, bool flip = false) + public void Draw(Camera cam, Vector3 pos, Vector2 origin, float rotate, Vector2 scale, Color color, bool flip = false, bool mirror = false) { if (Sprite.Texture == null) { return; } if (!initialized) { Init(); } @@ -291,6 +291,10 @@ namespace Barotrauma effect.Parameters["deformArray"].SetValue(deformAmount); effect.Parameters["deformArrayWidth"].SetValue(deformArrayWidth); effect.Parameters["deformArrayHeight"].SetValue(deformArrayHeight); + if (mirror) + { + flip = !flip; + } effect.Parameters["uvTopLeft"].SetValue(flip ? uvTopLeftFlipped : uvTopLeft); effect.Parameters["uvBottomRight"].SetValue(flip ? uvBottomRightFlipped : uvBottomRight); effect.GraphicsDevice.SetVertexBuffer(flip ? flippedVertexBuffer : vertexBuffer); @@ -368,7 +372,7 @@ namespace Barotrauma foreach (SpriteDeformation deformation in deformations) { - var deformEditor = new SerializableEntityEditor(container.RectTransform, deformation.DeformationParams, false, true); + var deformEditor = new SerializableEntityEditor(container.RectTransform, deformation.Params, false, true); deformEditor.RectTransform.MinSize = new Point(deformEditor.Rect.Width, deformEditor.Rect.Height); } diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs index ddf6f46b1..ed7b42e50 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs @@ -9,8 +9,9 @@ namespace Barotrauma { public partial class Sprite { - protected Texture2D texture; + private bool cannotBeLoaded; + protected Texture2D texture; public Texture2D Texture { get @@ -56,7 +57,7 @@ namespace Barotrauma public void EnsureLazyLoaded() { - if (!lazyLoad || texture != null) { return; } + if (!lazyLoad || texture != null || cannotBeLoaded) { return; } Vector4 sourceVector = Vector4.Zero; bool temp2 = false; @@ -74,6 +75,10 @@ namespace Barotrauma if (s == this) { continue; } if (s.FullPath == FullPath && s.texture != null) { s.texture = texture; } } + if (texture == null) + { + cannotBeLoaded = true; + } } public void ReloadTexture(bool updateAllSprites = false) => ReloadTexture(updateAllSprites ? LoadedSprites.Where(s => s.Texture == texture) : new Sprite[] { this }); @@ -93,10 +98,8 @@ namespace Barotrauma sourceRect = new Rectangle(0, 0, texture.Width, texture.Height); } - public static Texture2D LoadTexture(string file, bool preMultiplyAlpha = true) { - if (string.IsNullOrWhiteSpace(file)) { Texture2D t = null; @@ -109,7 +112,7 @@ namespace Barotrauma file = Path.GetFullPath(file); foreach (Sprite s in list) { - if (s.FullPath == file && s.texture != null) { return s.texture; } + if (s.FullPath == file && s.texture != null && !s.texture.IsDisposed) { return s.texture; } } if (File.Exists(file)) diff --git a/Barotrauma/BarotraumaClient/Source/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/Source/StatusEffects/StatusEffect.cs index 1842db331..0ba505f6a 100644 --- a/Barotrauma/BarotraumaClient/Source/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/Source/StatusEffects/StatusEffect.cs @@ -64,8 +64,14 @@ namespace Barotrauma { foreach (RoundSound sound in sounds) { + if (sound.Sound == null) + { + string errorMsg = $"Error in StatusEffect.ApplyProjSpecific1 (sound \"{sound.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace; + GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } soundChannel = SoundPlayer.PlaySound(sound.Sound, entity.WorldPosition, sound.Volume, sound.Range, hull); - if (soundChannel != null) soundChannel.Looping = loopSound; + if (soundChannel != null) { soundChannel.Looping = loopSound; } } } else @@ -84,8 +90,14 @@ namespace Barotrauma selectedSoundIndex = Rand.Int(sounds.Count); } var selectedSound = sounds[selectedSoundIndex]; + if (selectedSound.Sound == null) + { + string errorMsg = $"Error in StatusEffect.ApplyProjSpecific2 (sound \"{selectedSound.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace; + GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, entity.WorldPosition, selectedSound.Volume, selectedSound.Range, hull); - if (soundChannel != null) soundChannel.Looping = loopSound; + if (soundChannel != null) { soundChannel.Looping = loopSound; } } } diff --git a/Barotrauma/BarotraumaClient/Source/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/Source/Traitors/TraitorMissionPrefab.cs new file mode 100644 index 000000000..069f3a88c --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Traitors/TraitorMissionPrefab.cs @@ -0,0 +1,46 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + class TraitorMissionPrefab + { + public static readonly List List = new List(); + + public readonly string Identifier; + + public readonly Sprite Icon; + public readonly Color IconColor; + + public static void Init() + { + var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); + foreach (string file in files) + { + XDocument doc = XMLExtensions.TryLoadXml(file); + 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()) + { + if (subElement.Name.ToString().ToLowerInvariant() == "icon") + { + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs index d298dc51d..20dd90061 100644 --- a/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs @@ -80,16 +80,7 @@ namespace Barotrauma { int width = 0; int height = 0; int channels = 0; byte[] textureData = null; - Task loadTask = Task.Run(() => - { - textureData = Texture2D.TextureDataFromStream(fileStream, out width, out height, out channels); - }); - bool success = loadTask.Wait(10000); - if (!success) - { - DebugConsole.ThrowError("Failed to load texture data from " + (path ?? "stream") + ": timed out"); - return null; - } + textureData = Texture2D.TextureDataFromStream(fileStream, out width, out height, out channels); if (preMultiplyAlpha) { PreMultiplyAlpha(ref textureData); @@ -104,6 +95,10 @@ namespace Barotrauma } catch (Exception e) { +#if WINDOWS + if (e is SharpDX.SharpDXException) { throw; } +#endif + DebugConsole.ThrowError("Loading texture from stream failed!", e); return null; } diff --git a/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs index 37f56a618..6333203f8 100644 --- a/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs @@ -89,11 +89,22 @@ namespace Barotrauma return str; } - + public static Color GradientLerp(float t, params Color[] gradient) { - if (t <= 0.0f) return gradient[0]; - if (t >= 1.0f) return gradient[gradient.Length - 1]; + System.Diagnostics.Debug.Assert(gradient.Length > 0, "Empty color array passed to the GradientLerp method"); + if (gradient.Length == 0) + { +#if DEBUG + DebugConsole.ThrowError("Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace); +#endif + GameAnalyticsManager.AddErrorEventOnce("ToolBox.GradientLerp:EmptyColorArray", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace); + return Color.Black; + } + + if (t <= 0.0f) { return gradient[0]; } + if (t >= 1.0f) { return gradient[gradient.Length - 1]; } float scaledT = t * (gradient.Length - 1); diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 2e73b07c0..24fa2f47a 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -76,6 +76,9 @@ MinimumRecommendedRules.ruleset true + + app.manifest + @@ -279,6 +282,7 @@ + PreserveNewest diff --git a/Barotrauma/BarotraumaClient/app.manifest b/Barotrauma/BarotraumaClient/app.manifest new file mode 100644 index 000000000..ff31a5f7c --- /dev/null +++ b/Barotrauma/BarotraumaClient/app.manifest @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + + + + + + + diff --git a/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs index 888728512..862c08bd2 100644 --- a/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.9.3.2")] -[assembly: AssemblyFileVersion("0.9.3.2")] +[assembly: AssemblyVersion("0.9.4.0")] +[assembly: AssemblyFileVersion("0.9.4.0")] diff --git a/Barotrauma/BarotraumaServer/Server.csproj b/Barotrauma/BarotraumaServer/Server.csproj index 65d30a973..98cdb5a19 100644 --- a/Barotrauma/BarotraumaServer/Server.csproj +++ b/Barotrauma/BarotraumaServer/Server.csproj @@ -232,6 +232,7 @@ + diff --git a/Barotrauma/BarotraumaServer/Source/Characters/Character.cs b/Barotrauma/BarotraumaServer/Source/Characters/Character.cs index 8ad7eac64..fc7254410 100644 --- a/Barotrauma/BarotraumaServer/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/Source/Characters/Character.cs @@ -7,9 +7,7 @@ namespace Barotrauma { public static Character Controlled = null; - partial void InitProjSpecific(XDocument doc) - { - } + partial void InitProjSpecific(XElement mainElement) { } partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult) { diff --git a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs index c68552dc6..cb64ed2fb 100644 --- a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs @@ -205,10 +205,10 @@ namespace Barotrauma ResetAutoComplete(); break; } - + RewriteInputToCommandLine(input); } - + //TODO: be more clever about it Thread.Sleep(10); //sleep for 10ms to not pin the CPU super hard } @@ -249,7 +249,7 @@ namespace Barotrauma try { Console.WriteLine(""); Console.CursorTop -= inputLines; - + string ln = input.Length > 0 ? AutoComplete(input, 0) : ""; ln += new string(' ', consoleWidth - (ln.Length % consoleWidth)); Console.ForegroundColor = ConsoleColor.DarkGray; @@ -738,10 +738,22 @@ namespace Barotrauma }); AssignOnExecute("togglekarmatestmode|karmatestmode", (string[] args) => { - if (GameMain.Server?.KarmaManager == null) return; + if (GameMain.Server?.KarmaManager == null) { return; } GameMain.Server.KarmaManager.TestMode = !GameMain.Server.KarmaManager.TestMode; NewMessage(GameMain.Server.KarmaManager.TestMode ? "Karma test mode enabled." : "Karma test mode disabled.", Color.LightGreen); }); + AssignOnClientRequestExecute("togglekarmatestmode|karmatestmode", (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (GameMain.Server?.KarmaManager == null) { return; } + GameMain.Server.KarmaManager.TestMode = !GameMain.Server.KarmaManager.TestMode; + NewMessage(GameMain.Server.KarmaManager.TestMode ? + $"Karma test mode enabled by {client.Name}." : + $"Karma test mode disabled by {client.Name}.", + Color.LightGreen); + GameMain.Server.SendDirectChatMessage( + GameMain.Server.KarmaManager.TestMode ? "Karma test mode enabled." : "Karma test mode disabled.", + client); + }); AssignOnExecute("banendpoint", (string[] args) => { @@ -829,13 +841,13 @@ namespace Barotrauma AssignOnExecute("setclientcharacter", (string[] args) => { if (GameMain.Server == null) return; - + if (args.Length < 2) { ThrowError("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks."); return; } - + var client = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]); if (client == null) { @@ -844,6 +856,7 @@ namespace Barotrauma var character = FindMatchingCharacter(args.Skip(1).ToArray(), false); GameMain.Server.SetClientCharacter(client, character); + client.SpectateOnly = false; }); AssignOnExecute("difficulty|leveldifficulty", (string[] args) => @@ -939,7 +952,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.", "", TraitorMessageType.Console); return; } foreach (Traitor t in traitorManager.Traitors) @@ -953,11 +966,11 @@ namespace Barotrauma $"[traitorgoals]={traitorGoals.Substring(traitorGoalsStart)}", $"[traitorname]={t.Character.Name}", "Traitor [traitorname]'s current goals are:\n[traitorgoals]" - }.Where(s => !string.IsNullOrEmpty(s))), 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), "", TraitorMessageType.Console); } } //GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console); @@ -1053,7 +1066,7 @@ namespace Barotrauma commands.Add(new Command("servername", "servername [name]: Change the name of the server.", (string[] args) => { - GameMain.Server.Name = string.Join(" ", args); + GameMain.Server.ServerName = string.Join(" ", args); GameMain.NetLobbyScreen.ChangeServerName(string.Join(" ", args)); })); @@ -1160,7 +1173,7 @@ namespace Barotrauma { return new string[][] { - Submarine.Loaded.Select(s => s.Name).ToArray() + Submarine.SavedSubmarines.Select(s => s.Name).ToArray() }; })); @@ -1179,7 +1192,7 @@ namespace Barotrauma { return new string[][] { - Submarine.Loaded.Select(s => s.Name).ToArray() + Submarine.SavedSubmarines.Select(s => s.Name).ToArray() }; })); @@ -1195,7 +1208,7 @@ namespace Barotrauma commands.Add(new Command("startgame|startround|start", "start/startgame/startround: Start a new round.", (string[] args) => { - if (Screen.Selected == GameMain.GameScreen) return; + if (Screen.Selected == GameMain.GameScreen) { return; } if (!GameMain.Server.StartGame()) NewMessage("Failed to start a new round", Color.Yellow); })); @@ -1204,7 +1217,7 @@ namespace Barotrauma if (Screen.Selected == GameMain.NetLobbyScreen) return; GameMain.Server.EndGame(); })); - + commands.Add(new Command("entitydata", "", (string[] args) => { if (args.Length == 0) return; @@ -1528,7 +1541,7 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Character killedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); } ); @@ -1541,10 +1554,20 @@ namespace Barotrauma if (character != null) { GameMain.Server.SetClientCharacter(client, character); + client.SpectateOnly = false; } } ); + AssignOnClientRequestExecute( + "freecam", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + GameMain.Server.SetClientCharacter(client, null); + client.SpectateOnly = true; + } + ); + AssignOnClientRequestExecute( "difficulty|leveldifficulty", (Client client, Vector2 cursorWorldPos, string[] args) => @@ -1791,7 +1814,7 @@ namespace Barotrauma ThrowError("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks."); return; } - + var client = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]); if (client == null) { @@ -1800,6 +1823,7 @@ namespace Barotrauma var character = FindMatchingCharacter(args.Skip(1).ToArray(), false); GameMain.Server.SetClientCharacter(client, character); + client.SpectateOnly = false; } ); @@ -1867,7 +1891,7 @@ namespace Barotrauma foreach (Structure wall in Structure.WallList) { GameMain.Server.CreateEntityEvent(wall); - } + } })); #endif } diff --git a/Barotrauma/BarotraumaServer/Source/GameMain.cs b/Barotrauma/BarotraumaServer/Source/GameMain.cs index e00a75a9e..bea654413 100644 --- a/Barotrauma/BarotraumaServer/Source/GameMain.cs +++ b/Barotrauma/BarotraumaServer/Source/GameMain.cs @@ -48,7 +48,7 @@ namespace Barotrauma private static Stopwatch stopwatch; - public static HashSet SelectedPackages + public static IEnumerable SelectedPackages { get { return Config?.SelectedContentPackages; } } diff --git a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs index ca74513f9..56bdc6547 100644 --- a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -50,12 +50,18 @@ namespace Barotrauma { DebugConsole.ShowQuestionPrompt("Enter a save name for the campaign:", (string saveName) => { - StartNewCampaign(saveName, GameMain.NetLobbyScreen.SelectedSub.FilePath, GameMain.NetLobbyScreen.LevelSeed); + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); + StartNewCampaign(savePath, GameMain.NetLobbyScreen.SelectedSub.FilePath, GameMain.NetLobbyScreen.LevelSeed); }); } else { var saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer).ToArray(); + if (saveFiles.Length == 0) + { + DebugConsole.ThrowError("No save files found."); + return; + } DebugConsole.NewMessage("Saved campaigns:", Color.White); for (int i = 0; i < saveFiles.Length; i++) { @@ -64,9 +70,16 @@ namespace Barotrauma DebugConsole.ShowQuestionPrompt("Select a save file to load (0 - " + (saveFiles.Length - 1) + "):", (string selectedSave) => { int saveIndex = -1; - if (!int.TryParse(selectedSave, out saveIndex)) return; + if (!int.TryParse(selectedSave, out saveIndex)) { return; } - LoadCampaign(saveFiles[saveIndex]); + if (saveIndex < 0 || saveIndex >= saveFiles.Length) + { + DebugConsole.ThrowError("Invalid save file index."); + } + else + { + LoadCampaign(saveFiles[saveIndex]); + } }); } }); @@ -170,6 +183,7 @@ namespace Barotrauma msg.Write(Money); msg.Write(PurchasedHullRepairs); msg.Write(PurchasedItemRepairs); + msg.Write(PurchasedLostShuttles); msg.Write((UInt16)CargoManager.PurchasedItems.Count); foreach (PurchasedItem pi in CargoManager.PurchasedItems) @@ -196,6 +210,7 @@ namespace Barotrauma byte selectedMissionIndex = msg.ReadByte(); bool purchasedHullRepairs = msg.ReadBoolean(); bool purchasedItemRepairs = msg.ReadBoolean(); + bool purchasedLostShuttles = msg.ReadBoolean(); UInt16 purchasedItemCount = msg.ReadUInt16(); List purchasedItems = new List(); @@ -238,6 +253,24 @@ namespace Barotrauma Money += ItemRepairCost; } } + if (purchasedLostShuttles != this.PurchasedLostShuttles) + { + if (GameMain.GameSession?.Submarine != null && + GameMain.GameSession.Submarine.LeftBehindSubDockingPortOccupied) + { + GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); + } + else if (purchasedLostShuttles && Money >= ShuttleReplaceCost) + { + this.PurchasedLostShuttles = true; + Money -= ShuttleReplaceCost; + } + else if (!purchasedItemRepairs) + { + this.PurchasedLostShuttles = false; + Money += ShuttleReplaceCost; + } + } Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); if (Map.SelectedConnection != null) diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/ItemLabel.cs index e604c0a47..7bfb72b5c 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/ItemLabel.cs @@ -5,21 +5,21 @@ namespace Barotrauma.Items.Components { partial class ItemLabel : ItemComponent, IDrawableComponent { - [Serialize("", true), Editable(100)] + [Serialize("", true, description: "The text to display on the label."), Editable(100)] public string Text { get; set; } - [Editable, Serialize("0.0,0.0,0.0,1.0", true)] + [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label.")] public Color TextColor { get; set; } - [Editable, Serialize(1.0f, true)] + [Editable, Serialize(1.0f, true, description: "The scale of the text displayed on the label.")] public float TextScale { get; diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs index 2a777d279..28b81c2af 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { @@ -13,5 +14,101 @@ namespace Barotrauma.Items.Components get { return unsentChanges; } set { unsentChanges = value; } } + + + public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c) + { + bool autoPilot = msg.ReadBoolean(); + bool dockingButtonClicked = msg.ReadBoolean(); + Vector2 newSteeringInput = targetVelocity; + bool maintainPos = false; + Vector2? newPosToMaintain = null; + bool headingToStart = false; + + if (autoPilot) + { + maintainPos = msg.ReadBoolean(); + if (maintainPos) + { + newPosToMaintain = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + } + else + { + headingToStart = msg.ReadBoolean(); + } + } + else + { + newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + } + + if (!item.CanClientAccess(c)) return; + + user = c.Character; + AutoPilot = autoPilot; + + if (dockingButtonClicked) + { + item.SendSignal(0, "1", "toggle_docking", sender: null); + GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), true }); + } + + if (!AutoPilot) + { + steeringInput = newSteeringInput; + steeringAdjustSpeed = MathHelper.Lerp(0.2f, 1.0f, c.Character.GetSkillLevel("helm") / 100.0f); + } + else + { + MaintainPos = newPosToMaintain != null; + posToMaintain = newPosToMaintain; + + if (posToMaintain == null) + { + LevelStartSelected = headingToStart; + LevelEndSelected = !headingToStart; + UpdatePath(); + } + else + { + LevelStartSelected = false; + LevelEndSelected = false; + } + } + + //notify all clients of the changed state + unsentChanges = true; + } + + public void ServerWrite(IWriteMessage msg, Barotrauma.Networking.Client c, object[] extraData = null) + { + msg.Write(autoPilot); + msg.Write(extraData.Length > 2 && extraData[2] is bool && (bool)extraData[2]); + + if (!autoPilot) + { + //no need to write steering info if autopilot is controlling + msg.Write(steeringInput.X); + msg.Write(steeringInput.Y); + msg.Write(targetVelocity.X); + msg.Write(targetVelocity.Y); + msg.Write(steeringAdjustSpeed); + } + else + { + msg.Write(posToMaintain != null); + if (posToMaintain != null) + { + msg.Write(((Vector2)posToMaintain).X); + msg.Write(((Vector2)posToMaintain).Y); + } + else + { + msg.Write(LevelStartSelected); + } + } + } } } diff --git a/Barotrauma/BarotraumaServer/Source/Items/Item.cs b/Barotrauma/BarotraumaServer/Source/Items/Item.cs index 290ad0804..747efe98f 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Item.cs @@ -192,7 +192,7 @@ namespace Barotrauma { return; } - Combine(combineTarget); + Combine(combineTarget, c.Character); break; } } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs index f4f30b70b..ee4663238 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs @@ -82,7 +82,9 @@ namespace Barotrauma.Networking partial void InitProjSpecific() { - JobPreferences = new List(JobPrefab.List.GetRange(0, Math.Min(JobPrefab.List.Count, 3))); + var jobs = JobPrefab.List.Values.ToList(); + // TODO: modding support? + JobPreferences = new List(jobs.GetRange(0, Math.Min(jobs.Count, 3))); VoipQueue = new VoipQueue(ID, true, true); GameMain.Server.VoipServer.RegisterQueue(VoipQueue); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs index 385882aed..fefda7b32 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs @@ -23,6 +23,19 @@ namespace Barotrauma.Networking get { return true; } } + private string serverName; + + public string ServerName + { + get { return serverName; } + set + { + if (string.IsNullOrEmpty(value)) { return; } + serverName = value.Replace(":", "").Replace(";", ""); + } + } + + private List connectedClients = new List(); //for keeping track of disconnected clients in case the reconnect shortly after @@ -39,7 +52,7 @@ namespace Barotrauma.Networking private ServerPeer serverPeer; public ServerPeer ServerPeer { get { return serverPeer; } } - + private DateTime refreshMasterTimer; private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); private bool registeredToMaster; @@ -58,7 +71,7 @@ namespace Barotrauma.Networking get; private set; } - + private bool initiatedStartGame; private CoroutineHandle startGameCoroutine; @@ -90,7 +103,7 @@ namespace Barotrauma.Networking { get { return entityEventManager; } } - + public TimeSpan UpdateInterval { get { return updateInterval; } @@ -113,12 +126,13 @@ namespace Barotrauma.Networking { name = name.Substring(0, NetConfig.ServerNameMaxLength); } - - this.name = name; - + + this.serverName = name; + LastClientListUpdateID = 0; serverSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); + KarmaManager.SelectPreset(serverSettings.KarmaPreset); if (!string.IsNullOrEmpty(password)) { serverSettings.SetPassword(password); @@ -129,7 +143,7 @@ namespace Barotrauma.Networking ownerSteamId = steamId; entityEventManager = new ServerEntityEventManager(this); - + CoroutineManager.StartCoroutine(StartServer(isPublic)); } @@ -141,12 +155,12 @@ namespace Barotrauma.Networking Log("Starting the server...", ServerLog.MessageType.ServerMessage); if (!ownerSteamId.HasValue || ownerSteamId.Value == 0) { - Log("Using Lidgren networking", ServerLog.MessageType.ServerMessage); + Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses SteamP2P networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); serverPeer = new LidgrenServerPeer(ownerKey, serverSettings); } else { - Log("Using SteamP2P", ServerLog.MessageType.ServerMessage); + Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, serverSettings); } @@ -169,14 +183,14 @@ namespace Barotrauma.Networking Log("Error while starting the server (" + e.Message + ")", ServerLog.MessageType.Error); System.Net.Sockets.SocketException socketException = e as System.Net.Sockets.SocketException; - + error = true; } if (error) { if (serverPeer != null) serverPeer.Close("Error while starting the server"); - + Environment.Exit(-1); yield return CoroutineStatus.Success; @@ -257,7 +271,7 @@ namespace Barotrauma.Networking newClient.AddKickVote(c); } } - + LastClientListUpdateID++; if (newClient.Connection == OwnerConnection) @@ -317,7 +331,7 @@ namespace Barotrauma.Networking var request = new RestRequest("masterserver3.php", Method.GET); request.AddParameter("action", "addserver"); - request.AddParameter("servername", name); + request.AddParameter("servername", serverName); request.AddParameter("serverport", Port); request.AddParameter("currplayers", connectedClients.Count); request.AddParameter("maxplayers", serverSettings.MaxPlayers); @@ -434,12 +448,12 @@ namespace Barotrauma.Networking public override void Update(float deltaTime) { #if CLIENT - if (ShowNetStats) netStats.Update(deltaTime); + if (ShowNetStats) { netStats.Update(deltaTime); } #endif - if (!started) return; + if (!started) { return; } base.Update(deltaTime); - + fileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); @@ -492,16 +506,16 @@ namespace Barotrauma.Networking { if (Level.Loaded?.EndOutpost != null) { - bool charactersInsideOutpost = connectedClients.Any(c => - c.Character != null && - !c.Character.IsDead && + bool charactersInsideOutpost = connectedClients.Any(c => + c.Character != null && + !c.Character.IsDead && c.Character.Submarine == Level.Loaded.EndOutpost); //level finished if the sub is docked to the outpost //or very close and someone from the crew made it inside the outpost - subAtLevelEnd = + subAtLevelEnd = Submarine.MainSub.DockedTo.Contains(Level.Loaded.EndOutpost) || - (Submarine.MainSub.AtEndPosition && charactersInsideOutpost); + (Submarine.MainSub.AtEndPosition && charactersInsideOutpost); } else { @@ -510,7 +524,12 @@ namespace Barotrauma.Networking } float endRoundDelay = 1.0f; - if (serverSettings.AutoRestart && isCrewDead) + if (TraitorManager?.ShouldEndRound ?? false) + { + endRoundDelay = 5.0f; + endRoundTimer += deltaTime; + } + else if (serverSettings.AutoRestart && isCrewDead) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; @@ -533,10 +552,14 @@ namespace Barotrauma.Networking { endRoundTimer = 0.0f; } - + if (endRoundTimer >= endRoundDelay) { - if (serverSettings.AutoRestart && isCrewDead) + if (TraitorManager?.ShouldEndRound ?? false) + { + Log("Ending round (a traitor completed their mission)", ServerLog.MessageType.ServerMessage); + } + else if (serverSettings.AutoRestart && isCrewDead) { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } @@ -572,8 +595,8 @@ namespace Barotrauma.Networking if (serverSettings.AutoRestart) { //autorestart if there are any non-spectators on the server (ignoring the server owner) - bool shouldAutoRestart = connectedClients.Any(c => - c.Connection != OwnerConnection && + bool shouldAutoRestart = connectedClients.Any(c => + c.Connection != OwnerConnection && (!c.SpectateOnly || !serverSettings.AllowSpectating)); if (shouldAutoRestart != autoRestartTimerRunning) @@ -629,7 +652,7 @@ namespace Barotrauma.Networking } } - IEnumerable kickAFK = connectedClients.FindAll(c => + IEnumerable kickAFK = connectedClients.FindAll(c => c.KickAFKTimer >= serverSettings.KickAFKTime && (OwnerConnection == null || c.Connection != OwnerConnection)); foreach (Client c in kickAFK) @@ -639,6 +662,9 @@ namespace Barotrauma.Networking serverPeer.Update(deltaTime); + //don't run the rest of the method if something in serverPeer.Update causes the server to shutdown + if (!started) { return; } + // if update interval has passed if (updateTimer < DateTime.Now) { @@ -662,7 +688,7 @@ namespace Barotrauma.Networking } GameAnalyticsManager.AddErrorEventOnce( - "GameServer.Update:ClientWriteFailed" + e.StackTrace, + "GameServer.Update:ClientWriteFailed" + e.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } @@ -705,7 +731,7 @@ namespace Barotrauma.Networking serverSettings.ServerDetailsChanged = false; } } - + private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { var connectedClient = connectedClients.Find(c => c.Connection == sender); @@ -748,7 +774,7 @@ namespace Barotrauma.Networking if (matchingSub == null) { SendDirectChatMessage( - TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName), + TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName), connectedClient, ChatMessageType.MessageBox); } else @@ -890,8 +916,8 @@ namespace Barotrauma.Networking c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID); c.LastRecvChatMsgID = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvChatMsgID, c.LastChatMsgQueueID); c.LastRecvClientListUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvClientListUpdate, LastClientListUpdateID); - - TryChangeClientName(c, inc.ReadString()); + + TryChangeClientName(c, inc); c.LastRecvCampaignSave = inc.ReadUInt16(); if (c.LastRecvCampaignSave > 0) @@ -907,7 +933,7 @@ namespace Barotrauma.Networking campaign.DiscardClientCharacterData(c); } - //the client has a campaign save for another campaign + //the client has a campaign save for another campaign //(the server started a new campaign and the client isn't aware of it yet?) if (campaign.CampaignID != campaignID) { @@ -963,7 +989,7 @@ namespace Barotrauma.Networking UInt16 lastRecvChatMsgID = inc.ReadUInt16(); UInt16 lastRecvEntityEventID = inc.ReadUInt16(); UInt16 lastRecvClientListUpdate = inc.ReadUInt16(); - + //last msgs we've created/sent, the client IDs should never be higher than these UInt16 lastEntityEventID = entityEventManager.Events.Count == 0 ? (UInt16)0 : entityEventManager.Events.Last().ID; @@ -1071,10 +1097,10 @@ namespace Barotrauma.Networking return; } - //clients are allowed to end the round by talking with the watchman in multiplayer + //clients are allowed to end the round by talking with the watchman in multiplayer //campaign even if they don't have the special permission bool peekBool = inc.ReadBoolean(); inc.BitPosition--; - if (command == ClientPermissions.ManageRound && peekBool && + if (command == ClientPermissions.ManageRound && peekBool && GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign) { if (!mpCampaign.AllowedToEndRound(sender.Character) && !sender.HasPermission(command)) @@ -1192,7 +1218,7 @@ namespace Barotrauma.Networking { msg.Write(saveFile); } - + serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); } else @@ -1267,10 +1293,10 @@ namespace Barotrauma.Networking { c.Character.ClientDisconnected = true; } - + ClientWriteLobby(c); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && GameMain.NetLobbyScreen.SelectedMode == campaign.Preset && NetIdUtils.IdMoreRecent(campaign.LastSaveID, c.LastRecvCampaignSave)) { @@ -1284,7 +1310,7 @@ namespace Barotrauma.Networking return; } } - + if (!fileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { fileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); @@ -1312,14 +1338,15 @@ namespace Barotrauma.Networking { outmsg.Write(subList[i].Name); outmsg.Write(subList[i].MD5Hash.ToString()); + outmsg.Write(subList[i].RequiredContentPackagesInstalled); } outmsg.Write(GameStarted); outmsg.Write(serverSettings.AllowSpectating); - + c.WritePermissions(outmsg); } - + private void ClientWriteIngame(Client c) { //don't send position updates to characters who are still midround syncing @@ -1392,7 +1419,7 @@ namespace Barotrauma.Networking while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) { var entity = c.PendingPositionUpdates.Peek(); - if (entity == null || entity.Removed || + if (entity == null || entity.Removed || (entity is Item item && item.PositionUpdateInterval == float.PositiveInfinity)) { c.PendingPositionUpdates.Dequeue(); @@ -1436,7 +1463,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } - + serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); //--------------------------------------------------------------------------- @@ -1476,24 +1503,25 @@ namespace Barotrauma.Networking DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } - - serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); - } + + serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); + } } private void WriteClientList(Client c, IWriteMessage outmsg) { bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate); if (!hasChanged) { return; } - + outmsg.Write((byte)ServerNetObject.CLIENT_LIST); outmsg.Write(LastClientListUpdateID); - + outmsg.Write((byte)connectedClients.Count); foreach (Client client in connectedClients) { outmsg.Write(client.ID); outmsg.Write(client.SteamID); + outmsg.Write(client.NameID); outmsg.Write(client.Name); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); outmsg.Write(client.Muted); @@ -1512,25 +1540,28 @@ namespace Barotrauma.Networking outmsg.Write((byte)ServerNetObject.SYNC_IDS); int settingsBytes = outmsg.LengthBytes; + int initialUpdateBytes = 0; + IWriteMessage settingsBuf = null; if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.LastRecvLobbyUpdate)) { outmsg.Write(true); outmsg.WritePadBits(); - + outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); - IWriteMessage settingsBuf = new ReadWriteMessage(); + settingsBuf = new ReadWriteMessage(); serverSettings.ServerWrite(settingsBuf, c); - outmsg.Write((UInt16)settingsBuf.LengthBytes); - outmsg.Write(settingsBuf.Buffer,0,settingsBuf.LengthBytes); + outmsg.Write(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); outmsg.Write(c.LastRecvLobbyUpdate < 1); if (c.LastRecvLobbyUpdate < 1) { isInitialUpdate = true; + initialUpdateBytes = outmsg.LengthBytes; ClientWriteInitial(c, outmsg); + initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; } outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); @@ -1572,7 +1603,7 @@ namespace Barotrauma.Networking int campaignBytes = outmsg.LengthBytes; var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && + campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) { outmsg.Write(true); @@ -1600,7 +1631,7 @@ namespace Barotrauma.Networking chatMessageBytes = outmsg.LengthBytes - outmsg.LengthBytes; outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); - + if (isInitialUpdate) { //the initial update may be very large if the host has a large number @@ -1619,13 +1650,23 @@ namespace Barotrauma.Networking { if (outmsg.LengthBytes > MsgConstants.MTU) { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")"; + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; errorMsg += " Client list size: " + clientListBytes + " bytes\n" + " Chat message size: " + chatMessageBytes + " bytes\n" + " Campaign size: " + campaignBytes + " bytes\n" + - " Settings size: " + settingsBytes + " bytes\n\n"; - DebugConsole.ThrowError(errorMsg); + " Settings size: " + settingsBytes + " bytes\n"; + if (initialUpdateBytes > 0) + { + errorMsg += + " Initial update size: " + settingsBuf.LengthBytes + " bytes\n"; + } + if (settingsBuf != null) + { + errorMsg += + " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; + } + DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } @@ -1649,6 +1690,8 @@ namespace Barotrauma.Networking public bool StartGame() { + if (initiatedStartGame || gameStarted) { return false; } + Log("Starting a new round...", ServerLog.MessageType.ServerMessage); Submarine selectedSub = null; @@ -1691,7 +1734,7 @@ namespace Barotrauma.Networking private IEnumerable InitiateStartGame(Submarine selectedSub, Submarine selectedShuttle, bool usingShuttle, GameModePreset selectedMode) { initiatedStartGame = true; - + if (connectedClients.Any()) { IWriteMessage msg = new WriteOnlyMessage(); @@ -1705,7 +1748,7 @@ namespace Barotrauma.Networking msg.Write(selectedShuttle.MD5Hash.Hash); connectedClients.ForEach(c => c.ReadyToStart = false); - + foreach (NetworkConnection conn in connectedClients.Select(c => c.Connection)) { serverPeer.Send(msg, conn, DeliveryMethod.Reliable); @@ -1725,7 +1768,7 @@ namespace Barotrauma.Networking while (fileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) { waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime; - + yield return CoroutineStatus.Running; } } @@ -1739,7 +1782,7 @@ namespace Barotrauma.Networking private IEnumerable StartGame(Submarine selectedSub, Submarine selectedShuttle, bool usingShuttle, GameModePreset selectedMode) { entityEventManager.Clear(); - + roundStartSeed = DateTime.Now.Millisecond; Rand.SetSyncedSeed(roundStartSeed); @@ -1767,8 +1810,16 @@ namespace Barotrauma.Networking GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, (MissionType)GameMain.NetLobbyScreen.MissionTypeIndex); } + List playingClients = new List(connectedClients); + if (serverSettings.AllowSpectating) + { + playingClients.RemoveAll(c => c.SpectateOnly); + } + //always allow the server owner to spectate even if it's disallowed in server settings + playingClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); + if (GameMain.GameSession.GameMode.Mission != null && - GameMain.GameSession.GameMode.Mission.AssignTeamIDs(connectedClients)) + GameMain.GameSession.GameMode.Mission.AssignTeamIDs(playingClients)) { teamCount = 2; } @@ -1797,10 +1848,18 @@ namespace Barotrauma.Networking Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); } + if (GameMain.GameSession.Submarine.IsFileCorrupted) + { + CoroutineManager.StopCoroutines(startGameCoroutine); + initiatedStartGame = false; + SendChatMessage(TextManager.FormatServerMessage($"SubLoadError~[subname]={GameMain.GameSession.Submarine.Name}"), ChatMessageType.Error); + yield return CoroutineStatus.Failure; + } + MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); - if (serverSettings.AllowRespawn && missionAllowRespawn) respawnManager = new RespawnManager(this, usingShuttle ? selectedShuttle : null); + if (serverSettings.AllowRespawn && missionAllowRespawn) { respawnManager = new RespawnManager(this, usingShuttle ? selectedShuttle : null); } entityEventManager.RefreshEntityIDs(); @@ -1816,15 +1875,9 @@ namespace Barotrauma.Networking } //find the clients in this team - List teamClients = teamCount == 1 ? - new List(connectedClients) : - connectedClients.FindAll(c => c.TeamID == teamID); - if (serverSettings.AllowSpectating) - { - teamClients.RemoveAll(c => c.SpectateOnly); - } - //always allow the server owner to spectate even if it's disallowed in server settings - teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); + List teamClients = teamCount == 1 ? + new List(playingClients) : + playingClients.FindAll(c => c.TeamID == teamID); if (!teamClients.Any() && n > 0) { continue; } @@ -1843,7 +1896,7 @@ namespace Barotrauma.Networking if (client.CharacterInfo == null) { - client.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, client.Name); + client.CharacterInfo = new CharacterInfo(Character.HumanSpeciesName, client.Name); } characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob) @@ -1851,12 +1904,12 @@ namespace Barotrauma.Networking client.CharacterInfo.Job = new Job(client.AssignedJob); } } - + List bots = new List(); int botsToSpawn = serverSettings.BotSpawnMode == BotSpawnMode.Fill ? serverSettings.BotCount - characterInfos.Count : serverSettings.BotCount; for (int i = 0; i < botsToSpawn; i++) { - var botInfo = new CharacterInfo(Character.HumanConfigFile); + var botInfo = new CharacterInfo(Character.HumanSpeciesName); characterInfos.Add(botInfo); bots.Add(botInfo); } @@ -1929,9 +1982,9 @@ namespace Barotrauma.Networking GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.GameScreen.Select(); - + Log("Round started.", ServerLog.MessageType.ServerMessage); - + gameStarted = true; initiatedStartGame = false; GameMain.ResetFrameTime(); @@ -1988,7 +2041,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.AllowRagdollButton); serverSettings.WriteMonsterEnabled(msg); - + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -2022,10 +2075,10 @@ namespace Barotrauma.Networking "[endsummary]=" + roundSummary.Substring(roundSummaryStart), "[endsummary]\n\n[endsummary.traitorinfo]" }.Where(s => !string.IsNullOrEmpty(s))); - + Mission mission = GameMain.GameSession.Mission; GameMain.GameSession.GameMode.End(endMessage); - + endRoundTimer = 0.0f; if (serverSettings.AutoRestart) @@ -2036,7 +2089,7 @@ namespace Barotrauma.Networking } if (serverSettings.SaveServerLogs) serverSettings.ServerLog.Save(); - + GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; entityEventManager.Clear(); @@ -2063,7 +2116,7 @@ namespace Barotrauma.Networking msg.Write(endMessage); msg.Write(mission != null && mission.Completed); msg.Write(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); - + foreach (Client client in connectedClients) { serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); @@ -2080,7 +2133,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RandomizeSettings(); } - + public override void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } @@ -2089,9 +2142,14 @@ namespace Barotrauma.Networking base.AddChatMessage(message); } - private bool TryChangeClientName(Client c, string newName) + private bool TryChangeClientName(Client c, IReadMessage inc) { - if (c == null || string.IsNullOrEmpty(newName)) { return false; } + UInt16 nameId = inc.ReadUInt16(); + string newName = inc.ReadString(); + + if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } + + c.NameID = nameId; newName = Client.SanitizeName(newName); if (newName == c.Name) { return false; } @@ -2107,13 +2165,13 @@ namespace Barotrauma.Networking SendDirectChatMessage("Could not change your name to \"" + newName + "\" (the name contains disallowed symbols).", c, ChatMessageType.MessageBox); return false; } - if (Homoglyphs.Compare(newName.ToLower(), Name.ToLower())) + if (Homoglyphs.Compare(newName.ToLower(), ServerName.ToLower())) { SendDirectChatMessage("Could not change your name to \"" + newName + "\" (too similar to the server's name).", c, ChatMessageType.MessageBox); return false; } } - + Client nameTaken = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); if (nameTaken != null) { @@ -2207,7 +2265,7 @@ namespace Barotrauma.Networking lidgrenConn.IPEndPoint.Address.ToString(); if (range) { ip = serverSettings.BanList.ToRange(ip); } } - + serverSettings.BanList.BanPlayer(client.Name, ip, reason, duration); } if (client.SteamID > 0) @@ -2280,7 +2338,7 @@ namespace Barotrauma.Networking { if (client.HasKickVoteFrom(c)) { previousPlayer.KickVoters.Add(c); } } - + serverPeer.Disconnect(client.Connection, targetmsg); client.Dispose(); connectedClients.Remove(client); @@ -2288,7 +2346,7 @@ namespace Barotrauma.Networking KarmaManager.OnClientDisconnected(client); UpdateVoteStatus(); - + SendChatMessage(msg, ChatMessageType.Server); UpdateCrewFrame(); @@ -2360,7 +2418,7 @@ namespace Barotrauma.Networking default: if (command != "") { - if (command.ToLower() == name.ToLower()) + if (command.ToLower() == serverName.ToLower()) { //a private message to the host if (OwnerConnection != null) @@ -2411,7 +2469,7 @@ namespace Barotrauma.Networking //msg sent by the server if (senderCharacter == null) { - senderName = name; + senderName = serverName; } else //msg sent by an AI character { @@ -2443,14 +2501,14 @@ namespace Barotrauma.Networking //msg sent by the server if (senderCharacter == null) { - senderName = name; + senderName = serverName; } else //sent by an AI character, not allowed when the game is not running { return; } } - else //msg sent by a client + else //msg sent by a client { //game not started -> clients can only send normal and private chatmessages if (type != ChatMessageType.Private) type = ChatMessageType.Default; @@ -2482,7 +2540,7 @@ namespace Barotrauma.Networking break; } - if (type == ChatMessageType.Server) + if (type == ChatMessageType.Server || type == ChatMessageType.Error) { senderName = null; senderCharacter = null; @@ -2570,7 +2628,7 @@ namespace Barotrauma.Networking } string myReceivedMessage = message.Text; - + if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); @@ -2603,7 +2661,7 @@ namespace Barotrauma.Networking Client.UpdateKickVotes(connectedClients); - var clientsToKick = connectedClients.FindAll(c => + var clientsToKick = connectedClients.FindAll(c => c.Connection != OwnerConnection && c.KickVoteCount >= connectedClients.Count * serverSettings.KickVoteRequiredRatio); foreach (Client c in clientsToKick) @@ -2641,7 +2699,7 @@ namespace Barotrauma.Networking msg.Write((byte)ServerNetObject.VOTE); serverSettings.Voting.ServerWrite(msg); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - + foreach (var c in recipients) { serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); @@ -2702,7 +2760,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } yield return null; - } + } SendClientPermissions(recipient, client); yield return CoroutineStatus.Success; @@ -2718,7 +2776,7 @@ namespace Barotrauma.Networking client.WritePermissions(msg); serverPeer.Send(msg, recipient.Connection, DeliveryMethod.Reliable); } - + public void GiveAchievement(Character character, string achievementIdentifier) { achievementIdentifier = achievementIdentifier.ToLowerInvariant(); @@ -2740,22 +2798,18 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); msg.Write(achievementIdentifier); - + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void SendTraitorMessage(Client client, string message, TraitorMessageType messageType) + public void SendTraitorMessage(Client client, string message, string missionIdentifier, TraitorMessageType messageType) { if (client == null) { return; } - if (!TraitorManager.IsTraitor(client.Character) && client.Connection != OwnerConnection) - { - return; - } - var msg = new WriteOnlyMessage(); + var msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.TRAITOR_MESSAGE); msg.Write((byte)messageType); + msg.Write(missionIdentifier ?? ""); msg.Write(message); - serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } @@ -2767,10 +2821,10 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.CHEATS_ENABLED); msg.Write(DebugConsole.CheatsEnabled); msg.WritePadBits(); - + foreach (Client c in connectedClients) { - serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); + serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); } } @@ -2844,15 +2898,17 @@ namespace Barotrauma.Networking List jobPreferences = new List(); int count = message.ReadByte(); + // TODO: modding support? for (int i = 0; i < Math.Min(count, 3); i++) { string jobIdentifier = message.ReadString(); - - JobPrefab jobPrefab = JobPrefab.List.Find(jp => jp.Identifier == jobIdentifier); - if (jobPrefab != null) jobPreferences.Add(jobPrefab); + if (JobPrefab.List.TryGetValue(jobIdentifier, out JobPrefab jobPrefab)) + { + jobPreferences.Add(jobPrefab); + } } - sender.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, sender.Name); + sender.CharacterInfo = new CharacterInfo(Character.HumanSpeciesName, sender.Name); sender.CharacterInfo.RecreateHead(headSpriteId, race, gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); //if the client didn't provide job preferences, we'll use the preferences that are randomly assigned in the Client constructor @@ -2865,10 +2921,11 @@ namespace Barotrauma.Networking public void AssignJobs(List unassigned) { + var jobList = JobPrefab.List.Values.ToList(); unassigned = new List(unassigned); Dictionary assignedClientCount = new Dictionary(); - foreach (JobPrefab jp in JobPrefab.List) + foreach (JobPrefab jp in jobList) { assignedClientCount.Add(jp, 0); } @@ -2916,7 +2973,7 @@ namespace Barotrauma.Networking { unassignedJobsFound = false; - foreach (JobPrefab jobPrefab in JobPrefab.List) + foreach (JobPrefab jobPrefab in jobList) { if (unassigned.Count == 0) break; if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) continue; @@ -2954,22 +3011,22 @@ namespace Barotrauma.Networking foreach (Client c in unassigned) { //find all jobs that are still available - var remainingJobs = JobPrefab.List.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma); + var remainingJobs = jobList.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma); //all jobs taken, give a random job if (remainingJobs.Count == 0) { DebugConsole.ThrowError("Failed to assign a suitable job for \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); - int jobIndex = Rand.Range(0, JobPrefab.List.Count); + int jobIndex = Rand.Range(0, jobList.Count); int skips = 0; - while (c.Karma < JobPrefab.List[jobIndex].MinKarma) + while (c.Karma < jobList[jobIndex].MinKarma) { jobIndex++; skips++; - if (jobIndex >= JobPrefab.List.Count) jobIndex -= JobPrefab.List.Count; - if (skips >= JobPrefab.List.Count) break; + if (jobIndex >= jobList.Count) jobIndex -= jobList.Count; + if (skips >= jobList.Count) break; } - c.AssignedJob = JobPrefab.List[jobIndex]; + c.AssignedJob = jobList[jobIndex]; assignedClientCount[c.AssignedJob]++; } else //some jobs still left, choose one of them by random @@ -2982,12 +3039,13 @@ namespace Barotrauma.Networking public void AssignBotJobs(List bots, Character.TeamType teamID) { + var jobList = JobPrefab.List.Values.ToList(); Dictionary assignedPlayerCount = new Dictionary(); - foreach (JobPrefab jp in JobPrefab.List) + foreach (JobPrefab jp in jobList) { assignedPlayerCount.Add(jp, 0); } - + //count the clients who already have characters with an assigned job foreach (Client c in connectedClients) { @@ -3005,7 +3063,7 @@ namespace Barotrauma.Networking List unassignedBots = new List(bots); foreach (CharacterInfo bot in bots) { - foreach (JobPrefab jobPrefab in JobPrefab.List) + foreach (JobPrefab jobPrefab in jobList) { if (jobPrefab.MinNumber < 1 || assignedPlayerCount[jobPrefab] >= jobPrefab.MinNumber) continue; bot.Job = new Job(jobPrefab); @@ -3019,12 +3077,12 @@ namespace Barotrauma.Networking foreach (CharacterInfo c in unassignedBots) { //find all jobs that are still available - var remainingJobs = JobPrefab.List.FindAll(jp => assignedPlayerCount[jp] < jp.MaxNumber); + var remainingJobs = jobList.FindAll(jp => assignedPlayerCount[jp] < jp.MaxNumber); //all jobs taken, give a random job if (remainingJobs.Count == 0) { 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 = new Job(JobPrefab.List[Rand.Range(0, JobPrefab.List.Count)]); + c.Job = Job.Random(); assignedPlayerCount[c.Job.Prefab]++; } else //some jobs still left, choose one of them by random @@ -3121,7 +3179,7 @@ namespace Barotrauma.Networking } } } - + partial class PreviousPlayer { public string Name; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs index a2bcd18d8..8b5227fdb 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs @@ -85,7 +85,7 @@ namespace Barotrauma private void SendKarmaNotifications(Client client, string debugKarmaChangeReason = "") { - //send a notification about karma changing if the karma has changed by x% within the last second + //send a notification about karma changing if the karma has changed by x% var clientMemory = GetClientMemory(client); float karmaChange = client.Karma - clientMemory.PreviousNotifiedKarma; @@ -110,8 +110,8 @@ namespace Barotrauma { GameMain.Server.SendDirectChatMessage(TextManager.Get(karmaChange < 0 ? "KarmaDecreasedUnknownAmount" : "KarmaIncreasedUnknownAmount"), client); } + clientMemory.PreviousNotifiedKarma = client.Karma; } - clientMemory.PreviousNotifiedKarma = client.Karma; } private void UpdateClient(Client client, float deltaTime) @@ -324,13 +324,12 @@ namespace Barotrauma if (damageAmount > 0) { if (StructureDamageKarmaDecrease <= 0.0f) { return; } - - if (GameMain.Server.TraitorManager?.Traitors != null) - { - if (GameMain.Server.TraitorManager.Traitors.Any(t => - t.Character == attacker && - t.CurrentObjective != null && - t.CurrentObjective.HasGoalsOfType())) + if (GameMain.Server.TraitorManager?.Traitors != null) + { + if (GameMain.Server.TraitorManager.Traitors.Any(t => + t.Character == attacker && + t.CurrentObjective != null && + t.CurrentObjective.IsAllowedToDamage(structure))) { //traitor tasked to flood the sub -> damaging structures is ok return; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 05e8e4c4d..bf755a911 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -68,12 +68,14 @@ namespace Barotrauma.Networking { if (netServer != null) { return; } - netPeerConfiguration = new NetPeerConfiguration("barotrauma"); - netPeerConfiguration.AcceptIncomingConnections = true; - netPeerConfiguration.AutoExpandMTU = false; - netPeerConfiguration.MaximumConnections = serverSettings.MaxPlayers * 2; - netPeerConfiguration.EnableUPnP = serverSettings.EnableUPnP; - netPeerConfiguration.Port = serverSettings.Port; + netPeerConfiguration = new NetPeerConfiguration("barotrauma") + { + AcceptIncomingConnections = true, + AutoExpandMTU = false, + MaximumConnections = serverSettings.MaxPlayers * 2, + EnableUPnP = serverSettings.EnableUPnP, + Port = serverSettings.Port + }; netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | @@ -96,16 +98,16 @@ namespace Barotrauma.Networking } } - public override void Close(string msg=null) + public override void Close(string msg = null) { if (netServer == null) { return; } - for (int i=pendingClients.Count-1;i>=0;i--) + for (int i = pendingClients.Count - 1; i >= 0; i--) { - RemovePendingClient(pendingClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg); } - for (int i=connectedClients.Count-1;i>=0;i--) + for (int i = connectedClients.Count - 1; i >= 0; i--) { Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); } @@ -255,7 +257,7 @@ namespace Barotrauma.Networking { if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.AuthenticationRequired.ToString()+"/ Received data message from unauthenticated client"); + RemovePendingClient(pendingClient, DisconnectReason.AuthenticationRequired, "Received data message from unauthenticated client"); } else if (inc.SenderConnection.Status != NetConnectionStatus.Disconnected && inc.SenderConnection.Status != NetConnectionStatus.Disconnecting) @@ -307,8 +309,7 @@ namespace Barotrauma.Networking PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection); if (pendingClient != null) { - disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"; - RemovePendingClient(pendingClient, disconnectMsg); + RemovePendingClient(pendingClient, DisconnectReason.Unknown, $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"); } } break; @@ -344,7 +345,7 @@ namespace Barotrauma.Networking !IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4()) && ownerKey == null || ownKey == 0 && ownKey != ownerKey) { - RemovePendingClient(pendingClient, DisconnectReason.InvalidName.ToString() + "/ The name \"" + name + "\" is invalid"); + RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid"); return; } } @@ -353,7 +354,7 @@ namespace Barotrauma.Networking bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; if (!isCompatibleVersion) { - RemovePendingClient(pendingClient, + RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); @@ -388,7 +389,7 @@ namespace Barotrauma.Networking if (missingPackages.Count == 1) { - RemovePendingClient(pendingClient, + RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); return; @@ -397,7 +398,7 @@ namespace Barotrauma.Networking { List packageStrs = new List(); missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - RemovePendingClient(pendingClient, + RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); return; @@ -423,7 +424,7 @@ namespace Barotrauma.Networking ServerAuth.StartAuthSessionResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != ServerAuth.StartAuthSessionResult.OK) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam auth session failed to start: " + authSessionStartState.ToString()); + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); return; } pendingClient.SteamID = steamId; @@ -436,7 +437,7 @@ namespace Barotrauma.Networking { if (pendingClient.SteamID != steamId) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ SteamID mismatch"); + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch"); return; } } @@ -466,7 +467,7 @@ namespace Barotrauma.Networking serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID.Value, banMsg, null); } serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.Connection.RemoteEndPoint.Address, banMsg, null); - RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+" /"+banMsg); + RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); return; } } @@ -497,7 +498,7 @@ namespace Barotrauma.Networking if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, pendingClient.SteamID ?? 0)) { - RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()); + RemovePendingClient(pendingClient, DisconnectReason.Banned, ""); return; } @@ -505,13 +506,15 @@ namespace Barotrauma.Networking if (connectedClients.Count >= serverSettings.MaxPlayers) { - RemovePendingClient(pendingClient, DisconnectReason.ServerFull.ToString()); + RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); } if (pendingClient.InitializationStep == ConnectionInitialization.Success) { - LidgrenConnection newConnection = new LidgrenConnection(pendingClient.Name, pendingClient.Connection, pendingClient.SteamID ?? 0); - newConnection.Status = NetworkConnectionStatus.Connected; + LidgrenConnection newConnection = new LidgrenConnection(pendingClient.Name, pendingClient.Connection, pendingClient.SteamID ?? 0) + { + Status = NetworkConnectionStatus.Connected + }; connectedClients.Add(newConnection); pendingClients.Remove(pendingClient); @@ -531,7 +534,7 @@ namespace Barotrauma.Networking pendingClient.TimeOut -= deltaTime; if (pendingClient.TimeOut < 0.0) { - RemovePendingClient(pendingClient, Lidgren.Network.NetConnection.NoResponseMessage); + RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage); } if (Timing.TotalTime < pendingClient.UpdateTime) { return; } @@ -555,7 +558,12 @@ namespace Barotrauma.Networking } break; } - +#if DEBUG + netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Server.SimulatedDuplicatesChance; + netPeerConfiguration.SimulatedMinimumLatency = GameMain.Server.SimulatedMinimumLatency; + netPeerConfiguration.SimulatedRandomLatency = GameMain.Server.SimulatedRandomLatency; + netPeerConfiguration.SimulatedLoss = GameMain.Server.SimulatedLoss; +#endif NetSendResult result = netServer.SendMessage(outMsg, pendingClient.Connection, NetDeliveryMethod.ReliableUnordered); if (result != NetSendResult.Sent && result != NetSendResult.Queued) { @@ -564,7 +572,7 @@ namespace Barotrauma.Networking //DebugConsole.NewMessage("sent update to pending client: "+result); } - private void RemovePendingClient(PendingClient pendingClient, string reason) + private void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg) { if (netServer == null) { return; } @@ -579,7 +587,7 @@ namespace Barotrauma.Networking pendingClient.AuthSessionStarted = false; } - pendingClient.Connection.Disconnect(reason); + pendingClient.Connection.Disconnect(reason + "/" + msg); } } @@ -612,7 +620,7 @@ namespace Barotrauma.Networking if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, steamID)) { - RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString() + "/ SteamID banned"); + RemovePendingClient(pendingClient, DisconnectReason.Banned, "SteamID banned"); return; } @@ -623,7 +631,7 @@ namespace Barotrauma.Networking } else { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication failed: " + status.ToString()); + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam authentication failed: " + status.ToString()); return; } } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index eb0253df4..43842c8f8 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -105,7 +105,7 @@ namespace Barotrauma.Networking for (int i = pendingClients.Count - 1; i >= 0; i--) { - RemovePendingClient(pendingClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg); } for (int i = connectedClients.Count - 1; i >= 0; i--) @@ -244,7 +244,7 @@ namespace Barotrauma.Networking { if (pendingClient != null) { - RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+"/ Banned"); + RemovePendingClient(pendingClient, DisconnectReason.Banned, "Banned"); } else if (connectedClient != null) { @@ -257,7 +257,7 @@ namespace Barotrauma.Networking if (pendingClient != null) { string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"; - RemovePendingClient(pendingClient, disconnectMsg); + RemovePendingClient(pendingClient, DisconnectReason.Unknown, disconnectMsg); } else if (connectedClient != null) { @@ -313,9 +313,11 @@ namespace Barotrauma.Networking if (OwnerConnection == null) { string ownerName = inc.ReadString(); - OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID); - OwnerConnection.Status = NetworkConnectionStatus.Connected; - + OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID) + { + Status = NetworkConnectionStatus.Connected + }; + OnInitializationComplete?.Invoke(OwnerConnection); } return; @@ -384,7 +386,7 @@ namespace Barotrauma.Networking if (!Client.IsValidName(name, serverSettings)) { - RemovePendingClient(pendingClient, DisconnectReason.InvalidName.ToString() + "/ The name \"" + name + "\" is invalid"); + RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid"); return; } @@ -392,7 +394,7 @@ namespace Barotrauma.Networking bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; if (!isCompatibleVersion) { - RemovePendingClient(pendingClient, + RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); @@ -427,7 +429,7 @@ namespace Barotrauma.Networking if (missingPackages.Count == 1) { - RemovePendingClient(pendingClient, + RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); return; @@ -436,7 +438,7 @@ namespace Barotrauma.Networking { List packageStrs = new List(); missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - RemovePendingClient(pendingClient, + RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); return; @@ -471,7 +473,7 @@ namespace Barotrauma.Networking string banMsg = "Failed to enter correct password too many times"; serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID, banMsg, null); - RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+"/ "+banMsg); + RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); return; } } @@ -502,21 +504,23 @@ namespace Barotrauma.Networking if (serverSettings.BanList.IsBanned(pendingClient.SteamID)) { - RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+"/ Initialization interrupted by ban"); + RemovePendingClient(pendingClient, DisconnectReason.Banned, "Initialization interrupted by ban"); return; } //DebugConsole.NewMessage("pending client status: " + pendingClient.InitializationStep); - if (connectedClients.Count >= serverSettings.MaxPlayers-1) + if (connectedClients.Count >= serverSettings.MaxPlayers - 1) { - RemovePendingClient(pendingClient, DisconnectReason.ServerFull.ToString()); + RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); } - + if (pendingClient.InitializationStep == ConnectionInitialization.Success) { - SteamP2PConnection newConnection = new SteamP2PConnection(pendingClient.Name, pendingClient.SteamID); - newConnection.Status = NetworkConnectionStatus.Connected; + SteamP2PConnection newConnection = new SteamP2PConnection(pendingClient.Name, pendingClient.SteamID) + { + Status = NetworkConnectionStatus.Connected + }; connectedClients.Add(newConnection); pendingClients.Remove(pendingClient); OnInitializationComplete?.Invoke(newConnection); @@ -525,7 +529,7 @@ namespace Barotrauma.Networking pendingClient.TimeOut -= Timing.Step; if (pendingClient.TimeOut < 0.0) { - RemovePendingClient(pendingClient, Lidgren.Network.NetConnection.NoResponseMessage); + RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage); } if (Timing.TotalTime < pendingClient.UpdateTime) { return; } @@ -562,13 +566,13 @@ namespace Barotrauma.Networking } } - private void RemovePendingClient(PendingClient pendingClient, string reason) + private void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg) { if (netServer == null) { return; } if (pendingClients.Contains(pendingClient)) { - SendDisconnectMessage(pendingClient.SteamID, reason); + SendDisconnectMessage(pendingClient.SteamID, reason + "/" + msg); pendingClients.Remove(pendingClient); @@ -610,9 +614,14 @@ namespace Barotrauma.Networking lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; break; } - +#if DEBUG + netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Server.SimulatedDuplicatesChance; + netPeerConfiguration.SimulatedMinimumLatency = GameMain.Server.SimulatedMinimumLatency; + netPeerConfiguration.SimulatedRandomLatency = GameMain.Server.SimulatedRandomLatency; + netPeerConfiguration.SimulatedLoss = GameMain.Server.SimulatedLoss; +#endif NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); - byte[] msgData = new byte[1500]; + byte[] msgData = new byte[msg.LengthBytes]; msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); lidgrenMsg.Write(conn.SteamID); lidgrenMsg.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs index 0463e349e..eab5e7431 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs @@ -43,7 +43,7 @@ namespace Barotrauma.Networking CharacterInfo botToRespawn = existingBots.Find(b => b.IsDead)?.Info; if (botToRespawn == null) { - botToRespawn = new CharacterInfo(Character.HumanConfigFile); + botToRespawn = new CharacterInfo(Character.HumanSpeciesName); } else { @@ -225,7 +225,7 @@ namespace Barotrauma.Networking //all characters are in Team 1 in game modes/missions with only one team. //if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked c.TeamID = Character.TeamType.Team1; - if (c.CharacterInfo == null) c.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, c.Name); + if (c.CharacterInfo == null) c.CharacterInfo = new CharacterInfo(Character.HumanSpeciesName, c.Name); } List characterInfos = clients.Select(c => c.CharacterInfo).ToList(); @@ -290,7 +290,7 @@ namespace Barotrauma.Networking var oxyTank = new Item(oxyPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(oxyTank, false); - divingSuit.Combine(oxyTank); + divingSuit.Combine(oxyTank, user: null); respawnItems.Add(oxyTank); } @@ -302,7 +302,7 @@ namespace Barotrauma.Networking var battery = new Item(batteryPrefab, pos, respawnSub); Spawner.CreateNetworkEvent(battery, false); - scooter.Combine(battery); + scooter.Combine(battery, user: null); respawnItems.Add(scooter); respawnItems.Add(battery); } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs index a2999b34d..9a218b8a4 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs @@ -42,7 +42,7 @@ namespace Barotrauma.Networking Whitelist.ServerAdminWrite(outMsg, c); } - public void ServerWrite(IWriteMessage outMsg,Client c) + public void ServerWrite(IWriteMessage outMsg, Client c) { outMsg.Write(ServerName); outMsg.Write(ServerMessageText); @@ -69,8 +69,8 @@ namespace Barotrauma.Networking outMsg.WritePadBits(); } } - - public void ServerRead(IReadMessage incMsg,Client c) + + public void ServerRead(IReadMessage incMsg, Client c) { if (!c.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -91,7 +91,7 @@ namespace Barotrauma.Networking if (ServerMessageText != serverMessageText) changed = true; ServerMessageText = serverMessageText; } - + if (flags.HasFlag(NetFlags.Properties)) { changed |= ReadExtraCargo(incMsg); @@ -169,7 +169,15 @@ namespace Barotrauma.Networking changed |= true; } - if (changed) GameMain.NetLobbyScreen.LastUpdateID++; + if (changed) + { + if (KarmaPreset == "custom") + { + GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); + GameMain.NetworkMember?.KarmaManager?.Save(); + } + GameMain.NetLobbyScreen.LastUpdateID++; + } } public void SaveSettings() @@ -220,7 +228,7 @@ namespace Barotrauma.Networking doc = XMLExtensions.TryLoadXml(SettingsFile); } - if (doc == null || doc.Root == null) + if (doc == null) { doc = new XDocument(new XElement("serversettings")); } @@ -249,7 +257,7 @@ namespace Barotrauma.Networking "192-255", "384-591", "1024-1279", - "19968-40959","13312-19903","131072-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK + "19968-40959","13312-19903","131072-15043983","15043985-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK }; string[] allowedClientNameCharsStr = doc.Root.GetAttributeStringArray("AllowedClientNameChars", defaultAllowedClientNameChars); @@ -333,6 +341,7 @@ namespace Barotrauma.Networking } XDocument doc = XMLExtensions.TryLoadXml(ClientPermissionsFile); + if (doc == null) { return; } foreach (XElement clientElement in doc.Root.Elements()) { string clientName = clientElement.GetAttributeString("name", ""); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs index 61c864935..c15bef58d 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs @@ -48,7 +48,7 @@ namespace Barotrauma.Steam // 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 // creation/authentication functions to maintain your player count. - instance.server.ServerName = server.Name; + instance.server.ServerName = server.ServerName; instance.server.MaxPlayers = server.ServerSettings.MaxPlayers; instance.server.Passworded = server.ServerSettings.HasPassword; instance.server.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName ?? ""; diff --git a/Barotrauma/BarotraumaServer/Source/Program.cs b/Barotrauma/BarotraumaServer/Source/Program.cs index 77e872c80..9e7989cd3 100644 --- a/Barotrauma/BarotraumaServer/Source/Program.cs +++ b/Barotrauma/BarotraumaServer/Source/Program.cs @@ -62,6 +62,23 @@ namespace Barotrauma static void CrashDump(GameMain game, string filePath, Exception exception) { + try + { + GameMain.Server?.ServerSettings?.SaveSettings(); + GameMain.Server?.ServerSettings?.BanList.Save(); + if (GameMain.Server?.ServerSettings?.KarmaPreset == "custom") + { + GameMain.Server?.KarmaManager?.SaveCustomPreset(); + GameMain.Server?.KarmaManager?.Save(); + } + } + //gotta catch them all, we don't want to crash while writing a crash report + catch (Exception e) + { + string errorMsg = "Exception thrown while writing a crash report: " + e.Message + "\n" + e.StackTrace; + GameAnalyticsManager.AddErrorEventOnce("CrashDump:FailedToSaveSettings", EGAErrorSeverity.Error, errorMsg); + } + int existingFiles = 0; string originalFilePath = filePath; while (File.Exists(filePath)) diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Goal.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Goal.cs index b74289e24..93806e349 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Goal.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Goal.cs @@ -9,7 +9,7 @@ namespace Barotrauma { public abstract class Goal { - public Traitor Traitor { get; private set; } + public HashSet Traitors { get; } = new HashSet(); public TraitorMission Mission { get; internal set; } public virtual string StatusTextId { get; set; } = "TraitorGoalStatusTextFormat"; @@ -21,13 +21,13 @@ namespace Barotrauma public virtual string StatusValueTextId => IsCompleted ? "complete" : "inprogress"; public virtual IEnumerable StatusTextKeys => new [] { "[infotext]", "[status]" }; - public virtual IEnumerable StatusTextValues => new [] { InfoText, TextManager.FormatServerMessage(StatusValueTextId) }; + public virtual IEnumerable StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; public virtual IEnumerable InfoTextKeys => new string[] { }; - public virtual IEnumerable InfoTextValues => new string[] { }; + public virtual IEnumerable InfoTextValues(Traitor traitor) => new string[] { }; public virtual IEnumerable CompletedTextKeys => new string[] { }; - public virtual IEnumerable CompletedTextValues => 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); @@ -35,20 +35,19 @@ namespace Barotrauma protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); protected internal virtual string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); - public virtual string StatusText => GetStatusText(Traitor, StatusTextId, StatusTextKeys, StatusTextValues); - public virtual string InfoText => GetInfoText(Traitor, InfoTextId, InfoTextKeys, InfoTextValues); + public virtual string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor)); + public virtual string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor)); - public virtual string CompletedText => CompletedTextId != null ? GetCompletedText(Traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues) : StatusText; + public virtual string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor); public abstract bool IsCompleted { get; } - public virtual bool IsStarted => Traitor != null; - public virtual bool CanBeCompleted => !(Traitor?.Character?.IsDead ?? true); - + public virtual bool IsStarted(Traitor traitor) => Traitors.Contains(traitor); + public virtual bool CanBeCompleted(ICollection traitors) => !Traitors.Any(traitor => traitor.Character?.IsDead ?? true); public virtual bool IsEnemy(Character character) => false; - + public virtual bool IsAllowedToDamage(Structure structure) => false; public virtual bool Start(Traitor traitor) { - Traitor = traitor; + Traitors.Add(traitor); return true; } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs index b0ffa4dbd..23e129954 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs @@ -14,7 +14,7 @@ namespace Barotrauma private readonly bool matchInventory; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]", "[tag]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { string.Format("{0:0}", DestroyPercent * 100.0f), tagPrefabName ?? "" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", DestroyPercent * 100.0f), tagPrefabName ?? "" }); private readonly float destroyPercent; private float DestroyPercent => destroyPercent; @@ -31,7 +31,7 @@ namespace Barotrauma int result = 0; foreach (var item in Item.ItemList) { - if (!matchInventory && item.FindParentInventory(inventory => inventory.Owner is Character && inventory.Owner != Traitor.Character) != null) + if (!matchInventory && Traitors.All(traitor => item.FindParentInventory(inventory => inventory.Owner is Character && inventory.Owner != traitor.Character) != null)) { continue; } @@ -42,7 +42,7 @@ namespace Barotrauma } else { - if (item.Submarine.TeamID != Traitor.Character.TeamID) { continue; } + if (Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) { continue; } } if (item.Condition <= 0.0f) diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs index 11b42f2d1..70970e954 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs @@ -24,40 +24,39 @@ namespace Barotrauma private string targetHullNameText; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[identifier]", "[target]", "[targethullname]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" }); - public override bool IsCompleted => target != null && target.ParentInventory == Traitor.Character.Inventory; - public override bool CanBeCompleted { - get + public override bool IsCompleted => target != null && Traitors.Any(traitor => traitor.Character.HasItem(target)); + public override bool CanBeCompleted(ICollection traitors) + { + if (!base.CanBeCompleted(traitors)) { - if (!base.CanBeCompleted) - { - return false; - } - if (target == null) - { - return true; - } - if (target.Removed) - { - return false; - } - if (target.Submarine == null) - { - if (!(target.ParentInventory?.Owner is Character)) - { - return false; - } - } - else - { - if (target.Submarine.TeamID != Traitor.Character.TeamID) - { - return false; - } - } - return true; + return false; } + if (target == null) + { + var targetPrefabCandidate = FindItemPrefab(identifier); + return targetPrefabCandidate != null && FindTargetContainer(traitors, targetPrefabCandidate) != null; + } + if (target.Removed) + { + return false; + } + if (target.Submarine == null) + { + if (!(target.ParentInventory?.Owner is Character)) + { + return false; + } + } + else + { + if (Traitors.All(traitor => target.Submarine.TeamID != traitor.Character.TeamID)) + { + return false; + } + } + return true; } public override bool IsEnemy(Character character) => base.IsEnemy(character) || (target != null && target.FindParentInventory(inventory => inventory == character.Inventory) != null); @@ -67,27 +66,51 @@ namespace Barotrauma return (ItemPrefab)MapEntityPrefab.List.Find(prefab => prefab is ItemPrefab && prefab.Identifier == identifier); } - protected Item FindRandomContainer(bool includeNew, bool includeExisting) + protected Item FindRandomContainer(ICollection traitors, ItemPrefab targetPrefabCandidate, bool includeNew, bool includeExisting) { - int itemsCount = Item.ItemList.Count; - int startIndex = TraitorMission.Random(itemsCount); - Item fallback = null; - for (int i = 0; i < itemsCount; ++i) + List suitableItems = new List(); + foreach (Item item in Item.ItemList) { - var item = Item.ItemList[(i + startIndex) % itemsCount]; - if (item.Submarine == null || item.Submarine.TeamID != Traitor.Character.TeamID) + if (item.Submarine == null || traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) { continue; } - if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(item.prefab.Identifier)) { - if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier) != null)) + if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null)) { - return item; + suitableItems.Add(item); } } } + + if (suitableItems.Count == 0) { return null; } + return suitableItems[TraitorMission.Random(suitableItems.Count)]; + } + + protected Item FindTargetContainer(ICollection traitors, ItemPrefab targetPrefabCandidate) + { + Item result = null; + if (preferNew) + { + result = FindRandomContainer(traitors, targetPrefabCandidate, true, false); + } + if (result == null) + { + result = FindRandomContainer(traitors, targetPrefabCandidate, allowNew, allowExisting); + } + if (result == null) + { + return null; + } + if (allowNew && !result.OwnInventory.IsFull()) + { + return result; + } + if (allowExisting && result.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null) + { + return result; + } return null; } @@ -97,6 +120,10 @@ namespace Barotrauma { return false; } + if (targetPrefab != null) + { + return true; + } targetPrefab = FindItemPrefab(identifier); if (targetPrefab == null) { @@ -104,22 +131,16 @@ namespace Barotrauma } var targetPrefabTextId = targetPrefab.GetItemNameTextId(); targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name; - targetContainer = null; - if (preferNew) - { - targetContainer = FindRandomContainer(true, false); - } - if (targetContainer == null) - { - targetContainer = FindRandomContainer(allowNew, allowExisting); - } + targetContainer = FindTargetContainer(Traitors, targetPrefab); if (targetContainer == null) { + targetPrefab = null; + targetContainer = null; return false; } var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId(); targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name; - var targetHullTextId = targetContainer.CurrentHull != null ? targetContainer.CurrentHull.prefab.GetHullNameTextId() : null; + var targetHullTextId = targetContainer.CurrentHull?.prefab.GetHullNameTextId(); targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName ?? ""; if (allowNew && !targetContainer.OwnInventory.IsFull()) { @@ -135,6 +156,12 @@ namespace Barotrauma { target = targetContainer.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier); } + else + { + targetPrefab = null; + targetContainer = null; + return false; + } return true; } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFloodPercentOfSub.cs index d61125e58..414bc00ce 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFloodPercentOfSub.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFloodPercentOfSub.cs @@ -11,7 +11,7 @@ namespace Barotrauma private readonly float minimumFloodingAmount; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { string.Format("{0:0}", minimumFloodingAmount * 100.0f) }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", minimumFloodingAmount * 100.0f) }); private bool isCompleted = false; public override bool IsCompleted => isCompleted; @@ -23,7 +23,7 @@ namespace Barotrauma var floodingAmount = 0.0f; foreach (Hull hull in Hull.hullList) { - if (hull.Submarine == null || hull.Submarine.IsOutpost || hull.Submarine.TeamID != Traitor.Character.TeamID) { continue; } + if (hull.Submarine == null || hull.Submarine.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; } ++validHullsCount; floodingAmount += hull.WaterVolume / hull.Volume; diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs index 38511ac3d..e8fad3b79 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs @@ -12,7 +12,7 @@ namespace Barotrauma public Character Target { get; private set; } public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { Target?.Name ?? "(unknown)" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { Target?.Name ?? "(unknown)" }); private bool isCompleted = false; public override bool IsCompleted => isCompleted; diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs index e259e2182..32c93a7f3 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs @@ -14,20 +14,23 @@ namespace Barotrauma private readonly float requiredDistanceSqr; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[distance]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { $"{requiredDistance:0.00}" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredDistance:0.00}" }); public override bool IsCompleted { get { - if (Traitor == null || Traitor.Character == null || Traitor.Character.Submarine == null) + return Traitors.Any(traitor => { - return false; - } - var characterPosition = Traitor.Character.WorldPosition; - var submarinePosition = Traitor.Character.Submarine.WorldPosition; - var distance = Vector2.DistanceSquared(characterPosition, submarinePosition); - return distance >= requiredDistanceSqr; + if (traitor.Character?.Submarine == null) + { + return false; + } + var characterPosition = traitor.Character.WorldPosition; + var submarinePosition = traitor.Character.Submarine.WorldPosition; + var distance = Vector2.DistanceSquared(characterPosition, submarinePosition); + return distance >= requiredDistanceSqr; + }); } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReplaceInventory.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReplaceInventory.cs index f737e1c04..7df50ea26 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReplaceInventory.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReplaceInventory.cs @@ -17,7 +17,7 @@ namespace Barotrauma public override bool IsCompleted => isCompleted; public override IEnumerable StatusTextKeys => base.StatusTextKeys.Concat(new string[] { "[percentage]" }); - public override IEnumerable StatusTextValues => base.StatusTextValues.Concat(new string[] { string.Format("{0:0}", replaceAmount * 100.0f) }); + public override IEnumerable StatusTextValues(Traitor traitor) => base.StatusTextValues(traitor).Concat(new string[] { string.Format("{0:0}", replaceAmount * 100.0f) }); public override void Update(float deltaTime) { @@ -25,7 +25,7 @@ namespace Barotrauma int totalAmount = 0, replacedAmount = 0; foreach (var item in Item.ItemList) { - if (item.Submarine == null || item.Submarine.TeamID != Traitor.Character.TeamID) + if (item.Submarine == null || Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) { continue; } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalSabotageItems.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalSabotageItems.cs index d2cb82fb6..b7e826cb2 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalSabotageItems.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalSabotageItems.cs @@ -12,7 +12,7 @@ namespace Barotrauma private readonly float conditionThreshold; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[tag]", "[target]", "[threshold]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { tag ?? "", targetItemPrefabName ?? "", string.Format("{0:0}", conditionThreshold) }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { tag ?? "", targetItemPrefabName ?? "", string.Format("{0:0}", conditionThreshold) }); private bool isCompleted = false; public override bool IsCompleted => isCompleted; @@ -28,7 +28,7 @@ namespace Barotrauma } foreach (var item in Item.ItemList) { - if (item.Submarine == null || item.Submarine.TeamID != Traitor.Character.TeamID) + if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) { continue; } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalWaitForTraitors.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalWaitForTraitors.cs new file mode 100644 index 000000000..02e344392 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalWaitForTraitors.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalWaitForTraitors : Goal + { + private readonly int requiredCount; + private int count = 0; + + public override bool IsCompleted => count >= requiredCount; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[remaining]", "[count]" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredCount - count}", $"{requiredCount}" }); + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + ++count; + return true; + } + + public GoalWaitForTraitors(int requiredCount) : base() + { + this.requiredCount = requiredCount; + InfoTextId = "TraitorGoalWaitForTraitorsInfoText"; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/HumanoidGoal.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/HumanoidGoal.cs index ee9329989..06261de00 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/HumanoidGoal.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/HumanoidGoal.cs @@ -10,7 +10,7 @@ namespace Barotrauma { return false; } - return Traitor?.Character?.IsHumanoid ?? false; + return traitor?.Character?.IsHumanoid ?? false; } } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasDuration.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasDuration.cs index 926403cee..c33841af9 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasDuration.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasDuration.cs @@ -14,12 +14,12 @@ namespace Barotrauma public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { $"{TimeSpan.FromSeconds(requiredDuration):g}" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString() }); 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) ? TextManager.FormatServerMessage(durationInfoTextId, new[] { "[infotext]", "[duration]" }, new[] { infoText, $"{TimeSpan.FromSeconds(requiredDuration):g}" }) : infoText; + return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, new[] { "[infotext]", "[duration]" }, new[] { infoText, requiredDuration.ToString() }) : infoText; } private bool isCompleted = false; diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs index 2c1472f92..48e38f060 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs @@ -12,7 +12,7 @@ namespace Barotrauma private readonly string timeLimitInfoTextId; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[timelimit]" }); - public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { $"{TimeSpan.FromSeconds(timeLimit):g}" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{TimeSpan.FromSeconds(timeLimit):g}" }); protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { @@ -20,7 +20,7 @@ namespace Barotrauma return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, new[] { "[infotext]", "[timelimit]" }, new[] { infoText, $"{TimeSpan.FromSeconds(timeLimit):g}" }) : infoText; } - public override bool CanBeCompleted => base.CanBeCompleted && (!IsStarted || timeRemaining > 0.0f); + public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && (!Traitors.Any(IsStarted) || timeRemaining > 0.0f); private float timeRemaining; diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalIsOptional.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalIsOptional.cs index 028766bbd..e749f0a3f 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalIsOptional.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalIsOptional.cs @@ -9,19 +9,17 @@ namespace Barotrauma { private readonly string optionalInfoTextId; - public override string StatusValueTextId => (base.IsStarted && !base.CanBeCompleted) ? "failed" : base.StatusValueTextId; + public override string StatusValueTextId => (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)) ? "failed" : base.StatusValueTextId; - public override IEnumerable StatusTextValues + public override IEnumerable StatusTextValues(Traitor traitor) { - get { - var values = base.StatusTextValues.ToArray(); - values[1] = TextManager.GetServerMessage(StatusValueTextId); - return values; - } + var values = base.StatusTextValues(traitor).ToArray(); + values[1] = TextManager.GetServerMessage(StatusValueTextId); + return values; } - public override bool IsCompleted => base.IsCompleted || (base.IsStarted && !base.CanBeCompleted); - public override bool CanBeCompleted => true; + public override bool IsCompleted => base.IsCompleted || (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)); + public override bool CanBeCompleted(ICollection traitors) => true; protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/Modifier.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/Modifier.cs index 825a8c785..522e5d661 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/Modifier.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/Modifier.cs @@ -30,25 +30,25 @@ namespace Barotrauma } public override IEnumerable StatusTextKeys => Goal.StatusTextKeys; - public override IEnumerable StatusTextValues => new [] { InfoText, TextManager.FormatServerMessage(StatusValueTextId) }; + public override IEnumerable StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; public override IEnumerable InfoTextKeys => Goal.InfoTextKeys; - public override IEnumerable InfoTextValues => Goal.InfoTextValues; + public override IEnumerable InfoTextValues(Traitor traitor) => Goal.InfoTextValues(traitor); public override IEnumerable CompletedTextKeys => Goal.CompletedTextKeys; - public override IEnumerable CompletedTextValues => Goal.CompletedTextValues; + public override IEnumerable CompletedTextValues(Traitor traitor) => Goal.CompletedTextValues(traitor); protected internal override string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetStatusText(traitor, textId, keys, values); protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetInfoText(traitor, textId, keys, values); protected internal override string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetCompletedText(traitor, textId, keys, values); - public override string StatusText => GetStatusText(Traitor, StatusTextId, StatusTextKeys, StatusTextValues); - public override string InfoText => GetInfoText(Traitor, InfoTextId, InfoTextKeys, InfoTextValues); - public override string CompletedText => CompletedTextId != null ? GetCompletedText(Traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues) : StatusText; + public override string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor)); + public override string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor)); + public override string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor); public override bool IsCompleted => Goal.IsCompleted; - public override bool IsStarted => base.IsStarted && Goal.IsStarted; - public override bool CanBeCompleted => base.CanBeCompleted && Goal.CanBeCompleted; + public override bool IsStarted(Traitor traitor) => base.IsStarted(traitor) && Goal.IsStarted(traitor); + public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && Goal.CanBeCompleted(traitors); public override bool IsEnemy(Character character) => base.IsEnemy(character) || Goal.IsEnemy(character); diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs index 7833f4f17..48c4819fe 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs @@ -21,9 +21,13 @@ namespace Barotrauma public bool IsCompleted => pendingGoals.Count <= 0; public bool IsPartiallyCompleted => completedGoals.Count > 0; public bool IsStarted { get; private set; } = false; - public bool CanBeCompleted => !IsStarted || pendingGoals.All(goal => goal.CanBeCompleted); + public bool CanBeStarted(ICollection traitors) => !IsStarted && allGoals.Any(goal => goal.CanBeCompleted(traitors)); + public bool CanBeCompleted => !IsStarted || pendingGoals.All(goal => goal.CanBeCompleted(goal.Traitors)); public bool IsEnemy(Character character) => pendingGoals.Any(goal => goal.IsEnemy(character)); + public bool IsAllowedToDamage(Structure structure) => pendingGoals.Any(goal => goal.IsAllowedToDamage(structure)); + + public readonly HashSet Roles = new HashSet(); public string InfoText { get; private set; } @@ -33,7 +37,7 @@ namespace Barotrauma string.Join("/", string.Join("/", activeGoals.Select((goal, index) => { - var statusText = goal.StatusText; + 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]" })}"; }).ToArray()), @@ -43,7 +47,7 @@ namespace Barotrauma string.Join("/", string.Join("/", allGoals.Select((goal, index) => { - var statusText = goal.StatusText; + 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]" })}"; }).ToArray()), @@ -127,28 +131,21 @@ namespace Barotrauma } IsStarted = true; - traitor.SendChatMessageBox(StartMessageText); - traitor.UpdateCurrentObjective(GoalInfos); + traitor.SendChatMessageBox(StartMessageText, traitor.Mission?.Identifier); + traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission?.Identifier); return true; } public void StartMessage() { - Traitor.SendChatMessage(StartMessageText); - } - - public void End(bool displayMessage) - { - if (displayMessage) - { - Traitor.SendChatMessageBox(EndMessageText); - } + Traitor.SendChatMessage(StartMessageText, Traitor.Mission?.Identifier); } public void EndMessage() { - Traitor.SendChatMessage(EndMessageText); + Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission?.Identifier); + Traitor.SendChatMessage(EndMessageText, Traitor.Mission?.Identifier); } public void Update(float deltaTime) @@ -171,28 +168,24 @@ namespace Barotrauma pendingGoals.RemoveAt(i); if (GameMain.Server != null) { - Traitor.SendChatMessage(goal.CompletedText); + Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission?.Identifier); if (pendingGoals.Count > 0) { - Traitor.SendChatMessageBox(goal.CompletedText); + Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission?.Identifier); } - Traitor.UpdateCurrentObjective(GoalInfos); + Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission?.Identifier); } } } } - public Objective(string infoText, int shuffleGoalsCount, params Goal[] goals) + public Objective(string infoText, int shuffleGoalsCount, ICollection roles, ICollection goals) { InfoText = infoText; this.shuffleGoalsCount = shuffleGoalsCount; + Roles.UnionWith(roles); allGoals.AddRange(goals); } - - public bool HasGoalsOfType() where T : Goal - { - return allGoals?.Any(g => g is T) ?? false; - } } } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Traitor.cs index 79d739b48..7e42cff1b 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Traitor.cs @@ -1,7 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; -using System.Collections.Generic; -using System.Linq; namespace Barotrauma { @@ -9,8 +6,8 @@ namespace Barotrauma { public readonly Character Character; - public string Role { get; private set; } - public TraitorMission Mission { get; private set; } + public string Role { get; } + public TraitorMission Mission { get; } public Objective CurrentObjective => Mission.GetCurrentObjective(this); public Traitor(TraitorMission mission, string role, Character character) @@ -30,37 +27,32 @@ namespace Barotrauma }, new string[] { codeWords, codeResponse }); - messageSender(greetingMessage); - // boxSender(greetingMessage); - // SendChatMessage(greetingMessage); - // SendChatMessageBox(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, TraitorMessageType.ServerMessageBox); + GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText, Mission?.Identifier, TraitorMessageType.ServerMessageBox); } } - public void SendChatMessage(string serverText) + public void SendChatMessage(string serverText, string iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - GameMain.Server.SendTraitorMessage(traitorClient, serverText, TraitorMessageType.Server); + GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.Server); } - public void SendChatMessageBox(string serverText) + public void SendChatMessageBox(string serverText, string iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); - GameMain.Server.SendTraitorMessage(traitorClient, serverText, TraitorMessageType.ServerMessageBox); + GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.ServerMessageBox); } - public void UpdateCurrentObjective(string objectiveText) + public void UpdateCurrentObjective(string objectiveText, string iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); Character.TraitorCurrentObjective = objectiveText; - GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective, TraitorMessageType.Objective); + GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective, iconIdentifier, TraitorMessageType.Objective); } } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs index a22702d4e..2f71ffe1c 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs @@ -24,6 +24,12 @@ namespace Barotrauma private readonly Dictionary traitorCountsBySteamId = new Dictionary(); private readonly Dictionary traitorCountsByEndPoint = new Dictionary(); + public bool ShouldEndRound + { + get; + set; + } + public int GetTraitorCount(Tuple steamIdAndEndPoint) { if (steamIdAndEndPoint.Item1 > 0 && traitorCountsBySteamId.TryGetValue(steamIdAndEndPoint.Item1, out var steamIdResult)) @@ -51,6 +57,16 @@ namespace Barotrauma return Traitors.Any(traitor => traitor.Character == character); } + public string GetTraitorRole(Character character) + { + var traitor = Traitors.FirstOrDefault(candidate => candidate.Character == character); + if (traitor == null) + { + return ""; + } + return traitor.Role; + } + public TraitorManager() { } @@ -60,18 +76,21 @@ namespace Barotrauma #if DISABLE_MISSIONS return; #endif - if (server == null) return; + if (server == null) { return; } + + ShouldEndRound = false; Traitor.TraitorMission.InitializeRandom(); this.server = server; - //TODO: configure countdowns in xml - startCountdown = MathHelper.Lerp(90.0f, 180.0f, (float)Traitor.TraitorMission.RandomDouble()); + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinStartDelay, server.ServerSettings.TraitorsMaxStartDelay, (float)Traitor.TraitorMission.RandomDouble()); traitorCountsBySteamId.Clear(); traitorCountsByEndPoint.Clear(); } public void Update(float deltaTime) { + if (ShouldEndRound) { return; } + #if DISABLE_MISSIONS return; #endif @@ -102,21 +121,20 @@ namespace Barotrauma missionCompleted = true; foreach (var traitor in mission.Value.Traitors.Values) { - traitor.UpdateCurrentObjective(""); + traitor.UpdateCurrentObjective("", mission.Value.Identifier); } } } if (gameShouldEnd) { GameMain.GameSession.WinningTeam = winningTeam; - GameMain.Server.EndGame(); + ShouldEndRound = true; return; } if (missionCompleted) { Missions.Clear(); - //TODO: configure countdowns in xml - startCountdown = MathHelper.Lerp(90.0f, 180.0f, (float)Traitor.TraitorMission.RandomDouble()); + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)Traitor.TraitorMission.RandomDouble()); } } else if (startCountdown > 0.0f && server.GameStarted) @@ -127,7 +145,7 @@ namespace Barotrauma int playerCharactersCount = server.ConnectedClients.Sum(client => client.Character != null && !client.Character.IsDead ? 1 : 0); if (playerCharactersCount < server.ServerSettings.TraitorsMinPlayerCount) { - startCountdown = 60.0f; + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)Traitor.TraitorMission.RandomDouble()); return; } if (GameMain.GameSession.Mission is CombatMission) @@ -141,10 +159,10 @@ namespace Barotrauma Missions.Add(teamId, mission); } } - var canBeStartedCount = Missions.Sum(mission => mission.Value.CanBeStarted(server, this, mission.Key, "traitor") ? 1 : 0); + var canBeStartedCount = Missions.Sum(mission => mission.Value.CanBeStarted(server, this, mission.Key) ? 1 : 0); if (canBeStartedCount >= Missions.Count) { - var startSuccessCount = Missions.Sum(mission => mission.Value.Start(server, this, mission.Key, "traitor") ? 1 : 0); + var startSuccessCount = Missions.Sum(mission => mission.Value.Start(server, this, mission.Key) ? 1 : 0); if (startSuccessCount >= Missions.Count) { return; @@ -155,9 +173,9 @@ namespace Barotrauma { var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); if (mission != null) { - if (mission.CanBeStarted(server, this, Character.TeamType.None, "traitor")) + if (mission.CanBeStarted(server, this, Character.TeamType.None)) { - if (mission.Start(server, this, Character.TeamType.None, "traitor")) + if (mission.Start(server, this, Character.TeamType.None)) { Missions.Add(Character.TeamType.None, mission); return; @@ -166,7 +184,7 @@ namespace Barotrauma } } Missions.Clear(); - startCountdown = 60.0f; + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)Traitor.TraitorMission.RandomDouble()); } } } @@ -178,23 +196,31 @@ namespace Barotrauma #endif if (GameMain.Server == null || !Missions.Any()) return ""; - return string.Join("\n\n", Missions.Select(mission => mission.Value.GlobalEndMessage)); + return TextManager.JoinServerMessages("\n\n", Missions.Select(mission => mission.Value.GlobalEndMessage).ToArray()); } - public static T WeightedRandom(ICollection collection, Func random, Func readSelectedWeight, Action writeSelectedWeight, int entryWeight, int selectionWeight) where T : class + public static T WeightedRandom(IList collection, int startIndex, int count, Func random, Func readSelectedWeight, Action writeSelectedWeight, int entryWeight, int selectionWeight) where T : class { - var count = collection.Count; if (count <= 0) { return null; } - var maxCount = entryWeight + collection.Max(readSelectedWeight); - var totalWeight = collection.Sum(entry => maxCount - readSelectedWeight(entry)); - var selected = random(totalWeight); - foreach (var entry in collection) + var maxWeight = readSelectedWeight(collection[startIndex]); + var totalWeight = entryWeight + maxWeight; + for (var i = 1; i < count; ++i) { + var weight = readSelectedWeight(collection[startIndex + i]); + maxWeight = Math.Max(maxWeight, weight); + totalWeight += weight; + } + maxWeight += entryWeight; + totalWeight = count * maxWeight - totalWeight; + var selected = random(totalWeight); + for(var i = 0; i < count; ++i) + { + var entry = collection[startIndex + i]; var weight = readSelectedWeight(entry); - selected -= maxCount; + selected -= maxWeight; selected += weight; if (selected <= 0) { @@ -204,5 +230,10 @@ namespace Barotrauma } return null; } + + public static T WeightedRandom(IList collection, Func random, Func readSelectedWeight, Action writeSelectedWeight, int entryWeight, int selectionWeight) where T : class + { + return WeightedRandom(collection, 0, collection.Count, random, readSelectedWeight, writeSelectedWeight, entryWeight, selectionWeight); + } } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs index d29884ce1..dbb5235db 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs @@ -1,5 +1,5 @@ -//#define SERVER_IS_TRAITOR -//#define ALLOW_SOLO_TRAITOR +//#define ALLOW_SOLO_TRAITOR +//#define ALLOW_NONHUMANOID_TRAITOR using System; using Barotrauma.Networking; @@ -7,6 +7,7 @@ using Lidgren.Network; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using Barotrauma.Extensions; namespace Barotrauma @@ -35,28 +36,12 @@ namespace Barotrauma public readonly Dictionary Traitors = new Dictionary(); + public delegate bool RoleFilter(Character character); + public readonly Dictionary Roles = new Dictionary(); + public string StartText { get; private set; } public string CodeWords { get; private set; } public string CodeResponse { get; private set; } - public string EndMessage { - get - { - if (!Traitors.TryGetValue("traitor", out Traitor traitor)) - { - return ""; - } - - if (pendingObjectives.Count <= 0) - { - if (completedObjectives.Count <= 0) return ""; - return completedObjectives[completedObjectives.Count - 1].EndMessageText; - } - else - { - return pendingObjectives[0].EndMessageText; - } - } - } public string GlobalEndMessageSuccessTextId { get; private set; } public string GlobalEndMessageSuccessDeadTextId { get; private set; } @@ -65,14 +50,14 @@ namespace Barotrauma public string GlobalEndMessageFailureDeadTextId { get; private set; } public string GlobalEndMessageFailureDetainedTextId { get; private set; } - private readonly string objectiveGoalInfoFormat = "[index]. [goalinfos]\n"; + public readonly string Identifier; public virtual IEnumerable GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; public virtual IEnumerable GlobalEndMessageValues { get { var isSuccess = completedObjectives.Count >= allObjectives.Count; return new string[] { - (Traitors.TryGetValue("traitor", out var traitor) ? traitor.Character?.Name : null) ?? "(unknown)", + string.Join(", ", Traitors.Values.Select(traitor => traitor.Character?.Name ?? "(unknown)")), (isSuccess ? completedObjectives.LastOrDefault() : pendingObjectives.FirstOrDefault())?.GoalInfos ?? "" }; } @@ -82,20 +67,19 @@ namespace Barotrauma { get { - if (!Traitors.TryGetValue("traitor", out Traitor traitor)) + if (Traitors.Any() && allObjectives.Count > 0) { - return ""; - } - - if (allObjectives.Count > 0) - { - var isSuccess = completedObjectives.Count >= allObjectives.Count; - var traitorIsDead = traitor.Character.IsDead; - var traitorIsDetained = traitor.Character.LockHands; - 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.JoinServerMessages("\n", + Traitors.Values.Select(traitor => + { + var isSuccess = completedObjectives.Count >= allObjectives.Count; + var traitorIsDead = traitor.Character.IsDead; + var traitorIsDetained = traitor.Character.LockHands; + 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()); + }).ToArray()); } return ""; } @@ -103,21 +87,27 @@ namespace Barotrauma public Objective GetCurrentObjective(Traitor traitor) { - return pendingObjectives.Count > 0 ? pendingObjectives[0] : null; + if (!Traitors.ContainsValue(traitor) || pendingObjectives.Count <= 0) + { + return null; + } + return pendingObjectives.Find(objective => objective.Roles.Contains(traitor.Role)); } - protected List> FindTraitorCandidates(GameServer server, Character.TeamType team, params string[] traitorRoles) + protected List> FindTraitorCandidates(GameServer server, Character.TeamType team, RoleFilter traitorRoleFilter) { var traitorCandidates = new List>(); -#if SERVER_IS_TRAITOR - if (server.Character != null) + foreach (Client c in server.ConnectedClients) { - traitorCandidates.Add(server.Character); - } - else + if (c.Character == null || c.Character.IsDead || c.Character.Removed || !traitorRoleFilter(c.Character) || + (team != Character.TeamType.None && c.Character.TeamID != team)) + { + continue; + } +#if !ALLOW_NONHUMANOID_TRAITOR + if (!c.Character.IsHumanoid) { continue; } #endif - { - traitorCandidates.AddRange(server.ConnectedClients.FindAll(c => c.Character != null && !c.Character.IsDead && (team == Character.TeamType.None || c.Character.TeamID == team)).ConvertAll(client => Tuple.Create(client, client.Character))); + traitorCandidates.Add(Tuple.Create(c, c.Character)); } return traitorCandidates; } @@ -132,73 +122,127 @@ namespace Barotrauma return characters; } - public virtual bool CanBeStarted(GameServer server, TraitorManager traitorManager, Character.TeamType team, params string[] traitorRoles) - { - var traitorCandidates = FindTraitorCandidates(server, team, traitorRoles); - if (traitorCandidates.Count <= 0) - { - return false; - } - var characters = FindCharacters(); -#if !ALLOW_SOLO_TRAITOR - if (characters.Count < 2) - { - return false; - } -#endif - return true; - } - - public virtual bool Start(GameServer server, TraitorManager traitorManager, Character.TeamType team, params string[] traitorRoles) + protected List>> AssignTraitors(GameServer server, TraitorManager traitorManager, Character.TeamType team) { List characters = FindCharacters(); - List> traitorCandidates = FindTraitorCandidates(server, team, traitorRoles); - if (traitorCandidates.Count <= 0) - { - return false; - } #if !ALLOW_SOLO_TRAITOR if (characters.Count < 2) { - return false; + return null; } #endif - CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); - CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); - Traitors.Clear(); - foreach (var role in traitorRoles) + var roleCandidates = new Dictionary>>(); + foreach (var role in Roles) { - var candidate = TraitorManager.WeightedRandom(traitorCandidates, Random, t => + roleCandidates.Add(role.Key, new HashSet>(FindTraitorCandidates(server, team, role.Value))); + if (roleCandidates[role.Key].Count <= 0) + { + return null; + } + } + var candidateRoleCounts = new Dictionary, int>(); + foreach (var candidateEntry in roleCandidates) + { + foreach (var candidate in candidateEntry.Value) + { + candidateRoleCounts[candidate] = candidateRoleCounts.TryGetValue(candidate, out var count) ? count + 1 : 1; + } + } + var unassignedRoles = new List(roleCandidates.Keys); + unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count); + var assignedCandidates = new List>>(); + while (unassignedRoles.Count > 0) + { + var currentRole = unassignedRoles[0]; + var availableCandidates = roleCandidates[currentRole].ToList(); + if (availableCandidates.Count <= 0) + { + break; + } + unassignedRoles.RemoveAt(0); + availableCandidates.Sort((a, b) => candidateRoleCounts[b] - candidateRoleCounts[a]); + unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count); + + int numCandidates = 1; + for (int i = 1; i < availableCandidates.Count && candidateRoleCounts[availableCandidates[i]] == candidateRoleCounts[availableCandidates[0]]; ++i) + { + ++numCandidates; + } + var selected = TraitorManager.WeightedRandom(availableCandidates, 0, numCandidates, Random, t => { var previousClient = server.FindPreviousClientData(t.Item1); return Math.Max( previousClient != null ? traitorManager.GetTraitorCount(previousClient) : 0, traitorManager.GetTraitorCount(Tuple.Create(t.Item1.SteamID, t.Item1.Connection?.EndPointString ?? ""))); - }, (t, c) => - { - traitorManager.SetTraitorCount(Tuple.Create(t.Item1.SteamID, t.Item1.Connection?.EndPointString ?? ""), c); - }, 2, 3); - traitorCandidates.Remove(candidate); + }, (t, c) => { traitorManager.SetTraitorCount(Tuple.Create(t.Item1.SteamID, t.Item1.Connection?.EndPointString ?? ""), c); }, 2, 3); - var traitor = new Traitor(this, role, candidate.Item2); - Traitors.Add(role, traitor); + assignedCandidates.Add(Tuple.Create(currentRole, selected)); + foreach (var candidate in roleCandidates.Values) + { + candidate.Remove(selected); + } + } + if (unassignedRoles.Count > 0) + { + return null; + } + return assignedCandidates; + } + + public virtual bool CanBeStarted(GameServer server, TraitorManager traitorManager, Character.TeamType team) + { + foreach (var role in Roles) + { + var candidates = FindTraitorCandidates(server, team, role.Value); + if (candidates.Count <= 0) + { + return false; + } + } + return AssignTraitors(server, traitorManager, team) != null; + } + + public virtual bool Start(GameServer server, TraitorManager traitorManager, Character.TeamType team) + { + var assignedCandidates = AssignTraitors(server, traitorManager, team); + if (assignedCandidates == null) + { + return false; } - var messages = new Dictionary>(); + Traitors.Clear(); + foreach (var candidate in assignedCandidates) + { + var traitor = new Traitor(this, candidate.Item1, candidate.Item2.Item1.Character); + Traitors.Add(candidate.Item1, traitor); + } + CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); + CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); + + if (pendingObjectives.Count <= 0 || !pendingObjectives[0].CanBeStarted(Traitors.Values)) + { + Traitors.Clear(); + return false; + } + + var pendingMessages = new Dictionary>(); + pendingMessages.Clear(); foreach (var traitor in Traitors.Values) { - messages[traitor] = new List(); - if (traitor.CurrentObjective == null) { continue; } - traitor.Greet(server, CodeWords, CodeResponse, message => messages[traitor].Add(message)); + pendingMessages.Add(traitor, new List()); } + foreach (var traitor in Traitors.Values) + { + traitor.Greet(server, CodeWords, CodeResponse, message => pendingMessages[traitor].Add(message)); + } + pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier))); + pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier))); - messages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message))); - Update(0.0f, GameMain.Server.EndGame); - messages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message))); + Update(0.0f, () => { GameMain.Server.TraitorManager.ShouldEndRound = true; }); #if SERVER foreach (var traitor in Traitors.Values) { - GameServer.Log(string.Format("{0} is the traitor and the current goals are:\n{1}", traitor.Character.Name, traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)"), ServerLog.MessageType.ServerMessage); + GameServer.Log($"{traitor.Character.Name} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage); } #endif return true; @@ -212,23 +256,41 @@ namespace Barotrauma { return; } + if (Traitors.Values.Any(traitor => traitor.Character?.IsDead ?? true)) + { + Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier)); + return; + } + var startedObjectives = new List(); foreach (var traitor in Traitors.Values) { - if (traitor.Character.IsDead) + startedObjectives.Clear(); + while (pendingObjectives.Count > 0) { - traitor.UpdateCurrentObjective(""); - } - } - int previousCompletedCount = completedObjectives.Count; - int startedCount = 0; - while (pendingObjectives.Count > 0) - { - var objective = pendingObjectives[0]; - if (!objective.IsStarted) - { - if (!objective.Start(Traitors["traitor"])) + var objective = GetCurrentObjective(traitor); + if (objective == null) { - pendingObjectives.RemoveAt(0); + // No more objectives left for traitor or waiting for another traitor's objective. + break; + } + if (!objective.IsStarted) + { + if (!objective.Start(traitor)) + { + //the mission fails if an objective cannot be started + if (completedObjectives.Count > 0) + { + objective.EndMessage(); + } + pendingObjectives.Clear(); + break; + } + startedObjectives.Add(objective); + } + objective.Update(deltaTime); + if (objective.IsCompleted) + { + pendingObjectives.Remove(objective); completedObjectives.Add(objective); if (pendingObjectives.Count > 0) { @@ -236,41 +298,19 @@ namespace Barotrauma } continue; } - ++startedCount; - } - objective.Update(deltaTime); - if (objective.IsCompleted) - { - pendingObjectives.RemoveAt(0); - completedObjectives.Add(objective); - if (pendingObjectives.Count > 0) + if (objective.IsStarted && !objective.CanBeCompleted) { objective.EndMessage(); + pendingObjectives.Clear(); } - continue; + break; } - if (!objective.CanBeCompleted) + if (pendingObjectives.Count > 0) { - objective.EndMessage(); - objective.End(true); - pendingObjectives.Clear(); - } - break; - } - int completedMax = completedObjectives.Count - 1; - for (int i = previousCompletedCount; i <= completedMax; ++i) - { - var objective = completedObjectives[i]; - objective.End(i < completedMax || pendingObjectives.Count > 0); - } - if (pendingObjectives.Count > 0) - { - if (startedCount > 0) - { - pendingObjectives[0].StartMessage(); + startedObjectives.ForEach(objective => objective.StartMessage()); } } - else if (completedObjectives.Count >= allObjectives.Count) + if (completedObjectives.Count >= allObjectives.Count) { foreach (var traitor in Traitors) { @@ -303,8 +343,9 @@ namespace Barotrauma #endif } - public TraitorMission(string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, params Objective[] objectives) + public TraitorMission(string identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) { + Identifier = identifier; StartText = startText; GlobalEndMessageSuccessTextId = globalEndMessageSuccessTextId; GlobalEndMessageSuccessDeadTextId = globalEndMessageSuccessDeadTextId; @@ -312,6 +353,10 @@ namespace Barotrauma GlobalEndMessageFailureTextId = globalEndMessageFailureTextId; GlobalEndMessageFailureDeadTextId = globalEndMessageFailureDeadTextId; GlobalEndMessageFailureDetainedTextId = globalEndMessageFailureDetainedTextId; + foreach (var role in roles) + { + Roles.Add(role.Key, role.Value); + } allObjectives.AddRange(objectives); pendingObjectives.AddRange(objectives); } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs index 686086ec5..bd7d03f5f 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Xml.Linq; using System.Linq; -using Barotrauma.Extensions; using Barotrauma.Networking; -namespace Barotrauma { - +namespace Barotrauma +{ class TraitorMissionPrefab { public class TraitorMissionEntry @@ -98,6 +98,7 @@ namespace Barotrauma { private static Dictionary targetFilters = new Dictionary() { { "job", (value, character) => value.Equals(character.Info.Job.Prefab.Identifier, StringComparison.OrdinalIgnoreCase) }, + { "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) } }; public Traitor.Goal Instantiate() @@ -256,7 +257,16 @@ namespace Barotrauma { } } - public class Objective + + public abstract class ObjectiveBase + { + public HashSet Roles { get; } = new HashSet(); + + public abstract void InstantiateGoals(); + public abstract Traitor.Objective Instantiate(IEnumerable roles); + } + + protected class Objective : ObjectiveBase { public string InfoText { get; internal set; } public string StartMessageTextId { get; internal set; } @@ -271,16 +281,24 @@ namespace Barotrauma { public readonly List Goals = new List(); - public Traitor.Objective Instantiate() + private List goalInstances = null; + + public override void InstantiateGoals() { - var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, Goals.ConvertAll(goal => { + goalInstances = Goals.ConvertAll(goal => + { var instance = goal.Instantiate(); if (instance == null) { GameServer.Log($"Failed to instantiate goal \"{goal.Type}\".", ServerLog.MessageType.Error); } return instance; - }).FindAll(goal => goal != null).ToArray()); + }).FindAll(goal => goal != null); + } + + public override Traitor.Objective Instantiate(IEnumerable roles) + { + var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, roles.ToArray(), goalInstances); if (StartMessageTextId != null) { result.StartMessageTextId = StartMessageTextId; @@ -316,14 +334,43 @@ namespace Barotrauma { return result; } } - /* - public class Role + + protected class WaitObjective : ObjectiveBase { - public string Job; + private Traitor.GoalWaitForTraitors sharedGoal; + + public override void InstantiateGoals() + { + sharedGoal = new Traitor.GoalWaitForTraitors(Roles.Count); + } + + public override Traitor.Objective Instantiate(IEnumerable roles) + { + return new Traitor.Objective("TraitorObjectiveInfoTextWaitForOtherTraitors", -1, roles.ToArray(), new[] { sharedGoal }); + } + + public WaitObjective(ICollection roles) + { + Roles.UnionWith(roles); + } } + public class Role + { + public readonly Traitor.TraitorMission.RoleFilter Filter; + + public Role(IEnumerable filters) + { + Filter = character => filters.All(filter => filter(character)); + } + + public Role() + { + Filter = character => true; + } + } public readonly Dictionary Roles = new Dictionary(); - */ + public readonly string Identifier; public readonly string StartText; public readonly string EndMessageSuccessText; @@ -333,11 +380,43 @@ namespace Barotrauma { public readonly string EndMessageFailureDeadText; public readonly string EndMessageFailureDetainedText; - public readonly List Objectives = new List(); + public readonly List Objectives = new List(); public Traitor.TraitorMission Instantiate() { + var objectivesWithSync = new List(); + var objectivesCount = Objectives.Count; + if (objectivesCount > 0) + { + var pendingRoles = new HashSet(); + var pendingCount = 1; + objectivesWithSync.Add(Objectives[0]); + pendingRoles.UnionWith(Objectives[0].Roles); + for (var i = 1; i < objectivesCount; ++i) + { + var objective = Objectives[i]; + if (pendingRoles.IsSupersetOf(objective.Roles)) + { + if (pendingCount > 1) + { + objectivesWithSync.Add(new WaitObjective(objective.Roles)); + } + pendingRoles.Clear(); + pendingCount = 0; + } + objectivesWithSync.Add(objective); + pendingRoles.UnionWith(objective.Roles); + ++pendingCount; + } + if (pendingCount > 1 && pendingRoles.IsSubsetOf(Roles.Keys)) + { + // TODO: If last objective includes only one traitor, other traitors will get the wrong end message. + objectivesWithSync.Add(new WaitObjective(Roles.Keys)); + } + } + return new Traitor.TraitorMission( + Identifier, StartText ?? "TraitorMissionStartMessage", EndMessageSuccessText ?? "TraitorObjectiveEndMessageSuccess", EndMessageSuccessDeadText ?? "TraitorObjectiveEndMessageSuccessDead", @@ -345,7 +424,12 @@ namespace Barotrauma { EndMessageFailureText ?? "TraitorObjectiveEndMessageFailure", EndMessageFailureDeadText ?? "TraitorObjectiveEndMessageFailureDead", EndMessageFailureDetainedText ?? "TraitorObjectiveEndMessageFailureDetained", - Objectives.ConvertAll(objective => objective.Instantiate()).ToArray()); + Roles.ToDictionary(kv => kv.Key, kv => kv.Value.Filter), + objectivesWithSync.SelectMany(objective => + { + objective.InstantiateGoals(); + return objective.Roles.Select(role => objective.Instantiate(new[] { role })); + }).ToArray()); } protected Goal LoadGoal(XElement goalRoot) @@ -354,10 +438,22 @@ namespace Barotrauma { return new Goal(goalType, goalRoot); } - protected Objective LoadObjective(XElement objectiveRoot) - { - var result = new Objective(); - result.ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1); + protected Objective LoadObjective(XElement objectiveRoot, string[] allRoles) + { + var allRolesSet = new HashSet(allRoles); + var result = new Objective + { + ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1) + }; + var objectiveRoles = objectiveRoot.GetAttributeStringArray("roles", allRoles); + if (!allRolesSet.IsSupersetOf(objectiveRoles)) + { + var unrecognized = new HashSet(objectiveRoles); + unrecognized.ExceptWith(allRoles); + GameServer.Log($"Undefined role(s) \"{string.Join(", ", unrecognized)}\" set for Objective.", ServerLog.MessageType.Error); + } + result.Roles.UnionWith(allRolesSet.Intersect(objectiveRoles)); + foreach (var element in objectiveRoot.Elements()) { using (var checker = new AttributeChecker(element)) @@ -410,7 +506,7 @@ namespace Barotrauma { break; } default: - GameServer.Log($"Unrecognized element \"{element.Name}\"under Objective.", ServerLog.MessageType.Error); + GameServer.Log($"Unrecognized element \"{element.Name}\" under Objective.", ServerLog.MessageType.Error); break; } } @@ -418,6 +514,18 @@ namespace Barotrauma { return result; } + protected Role LoadRole(XElement roleRoot) + { + var filters = new List(); + var jobs = roleRoot.GetAttributeStringArray("jobs", null); + 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))); + } + return new Role(filters); + } + public TraitorMissionPrefab(XElement missionRoot) { Identifier = missionRoot.GetAttributeString("identifier", null); @@ -427,6 +535,27 @@ namespace Barotrauma { { switch (element.Name.ToString().ToLowerInvariant()) { + case "role": + checker.Required("id"); + checker.Optional("jobs"); + Roles.Add(element.GetAttributeString("id", null), LoadRole(element)); + break; + } + } + } + if (!Roles.Any()) + { + Roles.Add("traitor", new Role()); + } + foreach (var element in missionRoot.Elements()) + { + using (var checker = new AttributeChecker(element)) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "role": + // handled above + break; case "startinfotext": checker.Required("id"); StartText = element.GetAttributeString("id", null); @@ -457,7 +586,7 @@ namespace Barotrauma { break; case "objective": { - var objective = LoadObjective(element); + var objective = LoadObjective(element, Roles.Keys.ToArray()); if (objective != null) { Objectives.Add(objective); diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 2a154bde9..f1f352a9a 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -55,7 +55,9 @@ - + + + @@ -63,15 +65,14 @@ + - - - - + + @@ -82,7 +83,6 @@ - @@ -106,6 +106,7 @@ + @@ -124,6 +125,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index 2f25acf5c..46ae210ba 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -4,7 +4,7 @@ name="Default" karmadecay="0.08" karmadecaythreshold="50" - karmaincrease="0.1" + karmaincrease="0.05" karmaincreasethreshold="50" structurerepairkarmaincrease="0.05" structuredamagekarmadecrease="0.08" @@ -27,7 +27,7 @@ name="Strict" karmadecay="0.08" karmadecaythreshold="50" - karmaincrease="0.08" + karmaincrease="0.04" karmaincreasethreshold="45" structurerepairkarmaincrease="0.05" structuredamagekarmadecrease="0.15" @@ -50,7 +50,7 @@ name="Custom" karmadecay="0.08" karmadecaythreshold="50" - karmaincrease="0.1" + karmaincrease="0.05" karmaincreasethreshold="50" structurerepairkarmaincrease="0.05" structuredamagekarmadecrease="0.08" diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml index ef45bf03e..ac897c815 100644 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml @@ -1,7 +1,7 @@ - + - + diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml index 1135fd55a..8fcbeb57a 100644 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml @@ -1,5 +1,5 @@ - + diff --git a/Barotrauma/BarotraumaShared/Mods/info.txt b/Barotrauma/BarotraumaShared/Mods/info.txt index c48fff272..d3ce905ba 100644 --- a/Barotrauma/BarotraumaShared/Mods/info.txt +++ b/Barotrauma/BarotraumaShared/Mods/info.txt @@ -24,11 +24,11 @@ Content Packages: submarine from the "Publish item" tab in the Workshop menu, and the game automatically creates a folder and content package for your mod. -Example: +Example A very simple content package could be configured as follows: - + @@ -39,6 +39,11 @@ Example: + Note that this mod has been configured as a "core package". Core packages are + packages that contain all the necessary files to make the game run, instead of + just adding some extra files on top of another content package. There can only + be one core package selected at a time. + This content package would replace all the items in the game with whatever items are configured in the "Mods/BestModEver/items.xml" file. It would also use a modified version of the human characters and have all the monsters in the game replaced with @@ -46,4 +51,35 @@ Example: a new event that spawns Cthulhu and removing the events that spawn monsters/items which aren't included in the mod. - It is also set to be used with the version 0.9.1.0 of the game. \ No newline at end of file + Note that the content package should be saved with the file name "filelist.xml" in + the Mods folder, in this case "Mods/BestModEver/filelist.xml". + +Non-core content packages + + Most mods are usually not core content packages, but instead add things to or + modify things in the Vanilla content package (= the default content of the game). + + Here's an example of a simple non-core package: + + + + + + This mod would simply add an extra item to the game (or items if there are multiple + ones configured in the potatogun.xml file). + +Overriding content + + You can also set your mods to override vanilla content without having to modify the + Vanilla content package. This can be done by using Override-elements in the xml + configuration files. For example, the content of the potatogun.xml file could be + something like this: + + + + ... + + + + This would mean that the item overrides an item that has the identifier "harpoongun", + i.e. replaces harpoon guns with the potato gun. \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedCode.projitems b/Barotrauma/BarotraumaShared/SharedCode.projitems index 50f93972a..c16a050b1 100644 --- a/Barotrauma/BarotraumaShared/SharedCode.projitems +++ b/Barotrauma/BarotraumaShared/SharedCode.projitems @@ -50,11 +50,12 @@ - - - - - + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedContent.projitems b/Barotrauma/BarotraumaShared/SharedContent.projitems index 56ac50329..a83760e28 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.projitems +++ b/Barotrauma/BarotraumaShared/SharedContent.projitems @@ -20,8 +20,7 @@ - - + @@ -64,6 +63,57 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -283,21 +333,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -346,13 +381,37 @@ PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest @@ -391,18 +450,18 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest - - PreserveNewest - PreserveNewest - - PreserveNewest - PreserveNewest @@ -496,9 +555,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -508,6 +564,7 @@ PreserveNewest + PreserveNewest @@ -722,6 +779,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + Never + PreserveNewest @@ -758,6 +824,117 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -1154,9 +1331,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -1193,39 +1367,21 @@ PreserveNewest - - PreserveNewest - PreserveNewest PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest PreserveNewest - - PreserveNewest - PreserveNewest - - PreserveNewest - PreserveNewest @@ -1415,18 +1571,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -1436,9 +1580,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -1454,9 +1595,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -1547,28 +1685,19 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest @@ -1619,7 +1748,7 @@ PreserveNewest - + PreserveNewest @@ -1658,9 +1787,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -1694,9 +1820,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -1877,9 +2000,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -1916,12 +2036,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -2439,13 +2553,16 @@ PreserveNewest - PreserveNewest + Never PreserveNewest - - PreserveNewest + + Never + + + Never @@ -2602,15 +2719,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -2746,30 +2854,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -2899,13 +2983,13 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs index 49d298d9e..80aa66cb1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs @@ -2,10 +2,10 @@ namespace Barotrauma { + public enum AIState { Idle, Attack, Escape, Eat } + abstract partial class AIController : ISteerable { - public enum AIState { Idle, Attack, GoTo, Escape, Eat } - public bool Enabled; public readonly Character Character; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs index 0d1742307..553150231 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs @@ -21,7 +21,9 @@ namespace Barotrauma /// /// How long does it take for the ai target to fade out if not kept alive. /// - public float FadeOutTime { get; private set; } = 3; + public float FadeOutTime { get; private set; } + + public bool Static { get; private set; } public float SoundRange { @@ -128,6 +130,19 @@ namespace Barotrauma MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange); MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange); FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime); + Static = element.GetAttributeBool("static", Static); + if (Static) + { + SightRange = MaxSightRange; + SoundRange = MaxSoundRange; + } + else + { + // Non-static ai targets must be kept alive by a custom logic (e.g. item components) + SightRange = MinSightRange; + SoundRange = MinSoundRange; + } + SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f); SonarLabel = element.GetAttributeString("sonarlabel", ""); string typeString = element.GetAttributeString("type", "Any"); @@ -143,6 +158,16 @@ namespace Barotrauma List.Add(this); } + public void Update(float deltaTime) + { + if (!Static && FadeOutTime > 0) + { + // The aitarget goes silent/invisible if the components don't keep it active + SightRange -= deltaTime * (MaxSightRange / FadeOutTime); + SoundRange -= deltaTime * (MaxSoundRange / FadeOutTime); + } + } + public bool IsWithinSector(Vector2 worldPosition) { if (sectorRad >= MathHelper.TwoPi) return true; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs index df302f351..e85432eef 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs @@ -33,44 +33,10 @@ namespace Barotrauma } } - public class TargetingPriority - { - public string TargetTag; - public AIState State; - public float Priority; - - public TargetingPriority(XElement element) - { - TargetTag = element.GetAttributeString("tag", "").ToLowerInvariant(); - Enum.TryParse(element.GetAttributeString("state", ""), out State); - Priority = element.GetAttributeFloat("priority", 0.0f); - } - - public TargetingPriority(string tag, AIState state, float priority) - { - TargetTag = tag; - State = state; - Priority = priority; - } - } - private const float UpdateTargetsInterval = 1.0f; private const float RaycastInterval = 1.0f; - private bool attackWhenProvoked; - - private Dictionary targetingPriorities = new Dictionary(); - - //the preference to attack a specific type of target (-1.0 - 1.0) - //0.0 = doesn't attack targets of the type - //positive values = attacks targets of this type - //negative values = escapes targets of this type - //private float attackRooms, attackHumans, attackWeaker, attackStronger, eatDeadPriority; - - //determines which characters are considered weaker/stronger - private float combatStrength; - private SteeringManager outsideSteering, insideSteering; private float updateTargetsTimer; @@ -78,8 +44,17 @@ namespace Barotrauma private float raycastTimer; private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; - - private bool aggressiveBoarding; + + public float CombatStrength => Character.Params.AI.CombatStrength; + + private float Sight => Character.Params.AI.Sight; + private float Hearing => Character.Params.AI.Hearing; + private float FleeHealthThreshold => Character.Params.AI.FleeHealthThreshold; + + private float AggressionGreed => Character.Params.AI.AggressionGreed; + private float AggressionHurt => Character.Params.AI.AggressionHurt; + + private bool AggressiveBoarding => Character.Params.AI.AggressiveBoarding; //a point in a wall which the Character is currently targeting private WallTarget wallTarget; @@ -92,6 +67,7 @@ namespace Barotrauma private set { _attackingLimb = value; + attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; if (Character.AnimController is FishAnimController fishController) { @@ -99,24 +75,14 @@ namespace Barotrauma } } } - - //flee when the health is below this value - private float fleeHealthThreshold; private AITargetMemory selectedTargetMemory; private float targetValue; private Dictionary targetMemories; - - //the eyesight of the NPC (0.0 = blind, 1.0 = sees every target within sightRange) - public float sight; - //how far the NPC can hear targets from (0.0 = deaf, 1.0 = hears every target within soundRange) - public float hearing; - + private float colliderSize; - private readonly float aggressiongreed; - private readonly float aggressionhurt; // TODO: expose? private readonly float priorityFearIncreasement = 2; private readonly float memoryFadeTime = 0.5f; @@ -128,7 +94,7 @@ namespace Barotrauma { get { - var targetingPriority = GetTargetingPriority("human"); + var targetingPriority = GetTargetingPriority(Character.HumanSpeciesName); return targetingPriority != null && targetingPriority.State == AIState.Attack && targetingPriority.Priority > 0.0f; } } @@ -142,11 +108,6 @@ namespace Barotrauma } } - public float CombatStrength - { - get { return combatStrength; } - } - public override bool CanEnterSubmarine { get @@ -167,19 +128,26 @@ namespace Barotrauma public bool Reverse { get; private set; } - public EnemyAIController(Character c, string file, string seed) : base(c) + public EnemyAIController(Character c, string seed) : base(c) { + if (c.IsHuman) + { + throw new Exception($"Tried to create an enemy ai controller for human!"); + } + string file = Character.GetConfigFilePath(c.SpeciesName); + if (!Character.TryGetConfigFile(file, out XDocument doc)) + { + throw new Exception($"Failed to load the config file for {c.SpeciesName} from {file}!"); + } + var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; targetMemories = new Dictionary(); steeringManager = outsideSteering; - XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null || doc.Root == null) return; - List aiElements = new List(); List aiCommonness = new List(); - foreach (XElement element in doc.Root.Elements()) + foreach (XElement element in mainElement.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != "ai") continue; + if (!element.Name.ToString().Equals("ai", StringComparison.OrdinalIgnoreCase)) { continue; } aiElements.Add(element); aiCommonness.Add(element.GetAttributeFloat("commonness", 1.0f)); } @@ -194,21 +162,7 @@ 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); - - combatStrength = aiElement.GetAttributeFloat("combatstrength", 1.0f); - attackWhenProvoked = aiElement.GetAttributeBool("attackwhenprovoked", false); - aggressiveBoarding = aiElement.GetAttributeBool("aggressiveboarding", false); - - sight = aiElement.GetAttributeFloat("sight", 0.0f); - hearing = aiElement.GetAttributeFloat("hearing", 0.0f); - - aggressionhurt = aiElement.GetAttributeFloat("aggressionhurt", 100f); - aggressiongreed = aiElement.GetAttributeFloat("aggressiongreed", 10f); - - fleeHealthThreshold = aiElement.GetAttributeFloat("fleehealththreshold", 0.0f); - + XElement aiElement = aiElements.Count == 1 ? aiElements[0] : ToolBox.SelectWeightedRandom(aiElements, aiCommonness, random); foreach (XElement subElement in aiElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -220,9 +174,6 @@ namespace Barotrauma case "swarmbehavior": SwarmBehavior = new SwarmBehavior(subElement, this); break; - case "targetpriority": - targetingPriorities.Add(subElement.GetAttributeString("tag", "").ToLowerInvariant(), new TargetingPriority(subElement)); - break; } } @@ -260,15 +211,9 @@ namespace Barotrauma break; } } - - private TargetingPriority GetTargetingPriority(string targetTag) - { - if (targetingPriorities.TryGetValue(targetTag, out TargetingPriority priority)) - { - return priority; - } - return null; - } + + private CharacterParams.AIParams AIParams => Character.Params.AI; + private CharacterParams.TargetParams GetTargetingPriority(string targetTag) => AIParams.GetTarget(targetTag, false); public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -309,6 +254,16 @@ namespace Barotrauma } } + if (targetIgnoreTimer > 0) + { + targetIgnoreTimer -= deltaTime; + } + else + { + ignoredTargets.Clear(); + targetIgnoreTimer = targetIgnoreTime; + } + UpdateTargetMemories(deltaTime); if (updateTargetsTimer > 0.0) { @@ -316,14 +271,14 @@ namespace Barotrauma } else { - UpdateTargets(Character, out TargetingPriority targetingPriority); - updateTargetsTimer = UpdateTargetsInterval; + UpdateTargets(Character, out CharacterParams.TargetParams targetingPriority); + updateTargetsTimer = UpdateTargetsInterval * Rand.Range(0.75f, 1.25f); if (SelectedAiTarget == null) { State = AIState.Idle; } - else if (Character.Health < fleeHealthThreshold && SwarmBehavior == null) + else if (Character.HealthPercentage < FleeHealthThreshold && SwarmBehavior == null) { // Don't flee from damage if in a swarm. State = AIState.Escape; @@ -342,12 +297,18 @@ namespace Barotrauma if (Character.Submarine == null) { - if (steeringManager != outsideSteering) outsideSteering.Reset(); + if (steeringManager != outsideSteering) + { + outsideSteering.Reset(); + } steeringManager = outsideSteering; } else { - if (steeringManager != insideSteering) insideSteering.Reset(); + if (steeringManager != insideSteering) + { + insideSteering.Reset(); + } steeringManager = insideSteering; } @@ -397,24 +358,22 @@ namespace Barotrauma SteerInsideLevel(deltaTime); - if (wallTarget != null) { return; } - - if (SelectedAiTarget != null) + if (SelectedAiTarget != null && SelectedAiTarget.Entity.Submarine == Character.Submarine) { + // Steer towards the target Vector2 targetSimPos = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition; - steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f); steeringManager.SteeringSeek(targetSimPos); } else { - //wander around randomly + // Wander around randomly if (Character.Submarine == null) { steeringManager.SteeringAvoid(deltaTime, colliderSize * 5.0f); } steeringManager.SteeringWander(0.5f); - } + } } #endregion @@ -569,7 +528,7 @@ namespace Barotrauma Character.AnimController.TargetDir = Character.WorldPosition.X < attackWorldPos.X ? Direction.Right : Direction.Left; } - if (aggressiveBoarding) + if (AggressiveBoarding) { //targeting a wall section that can be passed through -> steer manually through the hole if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex)) @@ -626,7 +585,7 @@ namespace Barotrauma } else { - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, true); return; } } @@ -641,7 +600,7 @@ namespace Barotrauma if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, true); return; } AttackingLimb = null; @@ -664,7 +623,7 @@ namespace Barotrauma } else { - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, true); return; } } @@ -678,10 +637,11 @@ namespace Barotrauma } break; case AIBehaviorAfterAttack.FallBackUntilCanAttack: + case AIBehaviorAfterAttack.FollowThroughUntilCanAttack: if (AttackingLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -691,7 +651,7 @@ namespace Barotrauma // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -706,7 +666,7 @@ namespace Barotrauma else { // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -714,17 +674,32 @@ namespace Barotrauma else { // Cooldown not yet expired -> steer away from the target - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } break; + case AIBehaviorAfterAttack.FollowThrough: + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); + return; case AIBehaviorAfterAttack.FallBack: default: - UpdateFallBack(attackWorldPos, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); return; } } + else + { + attackVector = null; + } + + if (!CanAttack()) + { + // Invalid target + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + return; + } if (canAttack) { @@ -732,13 +707,68 @@ namespace Barotrauma { AttackingLimb = GetAttackLimb(attackWorldPos); } - canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; + if (AttackingLimb == null) + { + if (wallTarget != null) + { + float d = ConvertUnits.ToDisplayUnits(colliderSize) * 10; + if (Vector2.DistanceSquared(Character.AnimController.MainLimb.WorldPosition, attackWorldPos) < d * d) + { + // No valid attack limb -> let's turn away + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + return; + } + } + canAttack = false; + } + else + { + canAttack = AttackingLimb.attack.CoolDownTimer <= 0; + } } float distance = 0; + Limb attackTargetLimb = null; if (canAttack) { + if (SelectedAiTarget.Entity is Character targetCharacter) + { + var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; + if (targetLimbType != LimbType.None) + { + attackTargetLimb = GetTargetLimb(AttackingLimb, targetLimbType, targetCharacter); + if (attackTargetLimb == null) + { + State = AIState.Idle; + return; + } + attackWorldPos = attackTargetLimb.WorldPosition; + } + } // Check that we can reach the target - distance = Vector2.Distance(AttackingLimb.WorldPosition, attackWorldPos); + Vector2 toTarget = attackWorldPos - AttackingLimb.WorldPosition; + if (SelectedAiTarget.Entity is Character targetC) + { + // Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform) + Vector2 margin = CalculateMargin(targetC.AnimController.Collider.LinearVelocity); + toTarget += margin; + } + else if (SelectedAiTarget.Entity is MapEntity e) + { + if (e.Submarine != null) + { + Vector2 margin = CalculateMargin(e.Submarine.Velocity); + toTarget += margin; + } + } + + Vector2 CalculateMargin(Vector2 targetVelocity) + { + float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); + return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot; + } + + distance = toTarget.Length(); canAttack = distance < AttackingLimb.attack.Range; if (!canAttack && !IsCoolDownRunning) { @@ -747,73 +777,82 @@ namespace Barotrauma _attackingLimb = null; } } - - // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. For example the Hammerhead steering with the claws when it should use the torso. - // If we always use the main limb, this causes the character to seek the target with it's torso/head, when it should not. For example Mudraptor steering with it's belly, when it should use it's head. - // So let's use the one that's closer to the attacking limb. - Limb steeringLimb; - var torso = Character.AnimController.GetLimb(LimbType.Torso); - var head = Character.AnimController.GetLimb(LimbType.Head); - if (AttackingLimb == null) + Limb steeringLimb = canAttack ? AttackingLimb : null; + if (steeringLimb == null) { - steeringLimb = head ?? torso; - } - else - { - if (head != null && torso != null) - { - steeringLimb = Vector2.DistanceSquared(AttackingLimb.SimPosition, head.SimPosition) < Vector2.DistanceSquared(AttackingLimb.SimPosition, torso.SimPosition) ? head : torso; - } - else + // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. For example the Hammerhead steering with the claws when it should use the torso. + // If we always use the main limb, this causes the character to seek the target with it's torso/head, when it should not. For example Mudraptor steering with it's belly, when it should use it's head. + // So let's use the one that's closer to the attacking limb. + var torso = Character.AnimController.GetLimb(LimbType.Torso); + var head = Character.AnimController.GetLimb(LimbType.Head); + if (AttackingLimb == null) { steeringLimb = head ?? torso; } - } - if (steeringLimb != null) - { - Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; - // Offset so that we don't overshoot the movement - Vector2 steerPos = attackSimPos + offset; - SteeringManager.SteeringSeek(steerPos, 10); - - if (SteeringManager is IndoorsSteeringManager indoorsSteering) + else { - if (indoorsSteering.CurrentPath != null && !indoorsSteering.IsPathDirty) + if (head != null && torso != null) { - if (indoorsSteering.CurrentPath.Unreachable) - { - if (selectedTargetMemory != null) - { - //wander around randomly and decrease the priority faster if no path is found - selectedTargetMemory.Priority -= deltaTime * memoryFadeTime * 10; - } - SteeringManager.SteeringWander(); - } - else if (indoorsSteering.CurrentPath.Finished) - { - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); - } - else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null) - { - wallTarget = null; - SelectedAiTarget = indoorsSteering.CurrentPath.CurrentNode.ConnectedDoor.Item.AiTarget; - } - else if (indoorsSteering.CurrentPath.NextNode?.ConnectedDoor != null) - { - wallTarget = null; - SelectedAiTarget = indoorsSteering.CurrentPath.NextNode.ConnectedDoor.Item.AiTarget; - } + steeringLimb = Vector2.DistanceSquared(AttackingLimb.SimPosition, head.SimPosition) < Vector2.DistanceSquared(AttackingLimb.SimPosition, torso.SimPosition) ? head : torso; + } + else + { + steeringLimb = head ?? torso; } } - else if (Character.CurrentHull == null) + } + + if (steeringLimb == null) + { + State = AIState.Idle; + return; + } + + Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; + // Offset so that we don't overshoot the movement + Vector2 steerPos = attackSimPos + offset; + SteeringManager.SteeringSeek(steerPos, 10); + + if (SteeringManager is IndoorsSteeringManager indoorsSteering) + { + if (indoorsSteering.CurrentPath != null && !indoorsSteering.IsPathDirty) { - SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f); + if (indoorsSteering.CurrentPath.Unreachable) + { + if (selectedTargetMemory != null) + { + //wander around randomly and decrease the priority faster if no path is found + selectedTargetMemory.Priority -= deltaTime * memoryFadeTime * 10; + } + SteeringManager.SteeringWander(); + } + else if (indoorsSteering.CurrentPath.Finished) + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); + } + else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null) + { + wallTarget = null; + SelectedAiTarget = indoorsSteering.CurrentPath.CurrentNode.ConnectedDoor.Item.AiTarget; + } + else if (indoorsSteering.CurrentPath.NextNode?.ConnectedDoor != null) + { + wallTarget = null; + SelectedAiTarget = indoorsSteering.CurrentPath.NextNode.ConnectedDoor.Item.AiTarget; + } } } + else if (Character.CurrentHull == null) + { + SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f); + } if (canAttack) { - UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance); + if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) + { + IgnoreTarget(SelectedAiTarget); + } } } @@ -848,12 +887,29 @@ namespace Barotrauma return false; } + + private bool CanAttack() => CanAttack(wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity); + + private bool CanAttack(Entity target) + { + if (target == null) { return false; } + if (target is Character ch) + { + if (Character.CurrentHull == null && ch.CurrentHull != null || Character.CurrentHull != null && ch.CurrentHull == null) + { + return false; + } + } + return true; + } + private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { AttackContext currentContext = Character.GetAttackContext(); - var target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; + Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; + if (!CanAttack(target)) { return null; } Limb selectedLimb = null; - float currentPriority = 0; + float currentPriority = -1; foreach (Limb limb in Character.AnimController.Limbs) { if (limb == ignoredLimb) { continue; } @@ -917,7 +973,7 @@ namespace Barotrauma { if (wall.SectionBodyDisabled(i)) { - if (aggressiveBoarding && CanPassThroughHole(wall, i)) + if (AggressiveBoarding && CanPassThroughHole(wall, i)) { //aggressive boarders always target holes they can pass through sectionIndex = i; @@ -951,33 +1007,48 @@ namespace Barotrauma public override void OnAttacked(Character attacker, AttackResult attackResult) { - updateTargetsTimer = Math.Min(updateTargetsTimer, 0.1f); + float reactionTime = Rand.Range(0.1f, 0.3f); + updateTargetsTimer = Math.Min(updateTargetsTimer, reactionTime); - if (attackResult.Damage > 0.0f && attackWhenProvoked) + if (attackResult.Damage > 0.0f && Character.Params.AI.AttackOnlyWhenProvoked) { - if (!(attacker is AICharacter) || (((AICharacter)attacker).AIController is HumanAIController)) + string tag = attacker.SpeciesName.ToLowerInvariant(); + if (AIParams.TryGetTarget(tag, out CharacterParams.TargetParams target)) { - targetingPriorities["human"] = new TargetingPriority("human", AIState.Attack, 100.0f); - targetingPriorities["room"] = new TargetingPriority("room", AIState.Attack, 100.0f); + target.State = AIState.Attack; + target.Priority = Math.Max(target.Priority, 100f); + } + else + { + AIParams.TryAddNewTarget(tag, AIState.Attack, 100f, out _); + } + // If the target is a human and the human is inside a submarine, also target rooms. (TODO: should we remove this?) + if (attacker.Submarine != null && attacker.IsHuman) + { + if (AIParams.TryGetTarget("room", out CharacterParams.TargetParams room)) + { + room.State = AIState.Attack; + room.Priority = 100f; + } } } LatchOntoAI?.DeattachFromBody(); Character.AnimController.ReleaseStuckLimbs(); - if (attacker == null || attacker.AiTarget == null) return; + if (attacker == null || attacker.AiTarget == null) { return; } AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget); - targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * aggressionhurt; + targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt; // Reduce the cooldown so that the character can react // Only allow to react once. Otherwise would attack the target with only a fraction of cooldown - if (SelectedAiTarget != attacker.AiTarget) + if (SelectedAiTarget != attacker.AiTarget && Character.Params.AI.RetaliateWhenTakingDamage) { foreach (var limb in Character.AnimController.Limbs) { if (limb.attack != null) { - limb.attack.CoolDownTimer *= 0.1f; + limb.attack.CoolDownTimer *= reactionTime; } } } @@ -986,9 +1057,9 @@ namespace Barotrauma // 10 dmg, 100 health -> 0.1 private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); - private void UpdateLimbAttack(float deltaTime, Limb limb, Vector2 attackSimPos, float distance = -1) + private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) { - if (SelectedAiTarget == null) { return; } + if (SelectedAiTarget == null) { return false; } if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -1000,33 +1071,36 @@ namespace Barotrauma } if (SelectedAiTarget.Entity is IDamageable damageTarget) { - float prevHealth = damageTarget.Health; - if (limb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance)) + if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { if (damageTarget.Health > 0) { // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * aggressiongreed; + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AggressionGreed; } else { selectedTargetMemory.Priority = 0; } + return true; } } + return false; } - private void UpdateFallBack(Vector2 attackWorldPos, float deltaTime) + private Vector2? attackVector = null; + private void UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough) { - Vector2 attackVector = attackWorldPos - WorldPosition; - float dist = attackVector.Length(); - float desiredDist = colliderSize * 2.0f; - if (dist < desiredDist) + if (attackVector == null) { - Vector2 attackDir = Vector2.Normalize(-attackVector); - if (!MathUtils.IsValid(attackDir)) attackDir = Vector2.UnitY; - steeringManager.SteeringManual(deltaTime, attackDir * (1.0f - (dist / 500.0f))); + attackVector = attackWorldPos - WorldPosition; } + Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); + if (!MathUtils.IsValid(attackDir)) + { + attackDir = Vector2.UnitY; + } + steeringManager.SteeringManual(deltaTime, attackDir); steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f); } @@ -1036,30 +1110,29 @@ namespace Barotrauma private void UpdateEating(float deltaTime) { - if (SelectedAiTarget == null) //SelectedAiTarget.Entity is Character c && !c.IsDead + if (SelectedAiTarget == null) { State = AIState.Idle; return; } - Character targetChar = SelectedAiTarget.Entity as Character; - - Limb mouthLimb = Array.Find(Character.AnimController.Limbs, l => l != null && l.MouthPos.HasValue); - if (mouthLimb == null) mouthLimb = Character.AnimController.GetLimb(LimbType.Head); + Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head); if (mouthLimb == null) { - DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (a head or a limb with a mouthpos required)"); + DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)"); State = AIState.Idle; return; } - Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value; Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition; - Vector2 limbDiff = attackSimPosition - mouthPos; float limbDist = limbDiff.Length(); if (limbDist < 2.0f) { - Character.SelectCharacter(SelectedAiTarget.Entity as Character); + if (SelectedAiTarget.Entity is Character c) + { + // TODO: what if we use this for eating something else than characters? + Character.SelectCharacter(c); + } steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff)); Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); } @@ -1077,7 +1150,7 @@ namespace Barotrauma //goes through all the AItargets, evaluates how preferable it is to attack the target, //whether the Character can see/hear the target and chooses the most preferable target within //sight/hearing range - public AITarget UpdateTargets(Character character, out TargetingPriority priority) + public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams priority) { if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub) { @@ -1119,7 +1192,9 @@ namespace Barotrauma foreach (AITarget target in AITarget.List) { - if (!target.Enabled) continue; + if (!target.Enabled) {continue; } + // Only ignore targets that are not in the same sub. + if (ignoredTargets.Contains(target) && target.Entity.Submarine != character.Submarine) { continue; } if (Level.Loaded != null && target.WorldPosition.Y > Level.Loaded.Size.Y) { continue; @@ -1153,11 +1228,16 @@ namespace Barotrauma } else if (targetCharacter.AIController is EnemyAIController enemy) { - if (enemy.combatStrength > combatStrength) + if (targetCharacter.Params.CompareGroup(Character.Params.Group)) + { + // Ignore targets that are in the same group (treat them like they were of the same species) + continue; + } + if (enemy.CombatStrength > CombatStrength) { targetingTag = "stronger"; } - else if (enemy.combatStrength < combatStrength) + else if (enemy.CombatStrength < CombatStrength) { targetingTag = "weaker"; } @@ -1180,12 +1260,12 @@ namespace Barotrauma } } } - else if (targetCharacter.Submarine != null && Character.Submarine == null) + else if (targetCharacter.Submarine != null && Character.Submarine == null && !AggressiveBoarding) { //target inside, AI outside -> we'll be attacking a wall between the characters so use the priority for attacking rooms targetingTag = "room"; } - else if (targetingPriorities.ContainsKey(targetCharacter.SpeciesName.ToLowerInvariant())) + else if (AIParams.Targets.Any(t => t.Tag.Equals(targetCharacter.SpeciesName, StringComparison.OrdinalIgnoreCase))) { targetingTag = targetCharacter.SpeciesName.ToLowerInvariant(); } @@ -1199,17 +1279,17 @@ namespace Barotrauma if (target.Entity is Item item) { //item inside and we're outside -> attack the hull - if (item.CurrentHull != null && character.CurrentHull == null) + if (item.CurrentHull != null && character.CurrentHull == null && !AggressiveBoarding) { targetingTag = "room"; } door = item.GetComponent(); - foreach (TargetingPriority prio in targetingPriorities.Values) + foreach (var prio in AIParams.Targets) { - if (item.HasTag(prio.TargetTag)) + if (item.HasTag(prio.Tag)) { - targetingTag = prio.TargetTag; + targetingTag = prio.Tag; break; } } @@ -1241,28 +1321,8 @@ namespace Barotrauma float wallMaxHealth = 400; // Anything more than this is ignored -> 200 = 1 // Prefer weaker targets. valueModifier *= MathHelper.Lerp(1.5f, 0.5f, MathUtils.InverseLerp(0, 1, s.Health / wallMaxHealth)); - if (aggressiveBoarding) - { - var hulls = s.Submarine.GetHulls(false); - for (int i = 0; i < s.Sections.Length; i++) - { - var section = s.Sections[i]; - if (section.gap != null) - { - if (CanPassThroughHole(s, i)) - { - bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null && hulls.Any(h => h.Rect.Intersects(section.rect)); - valueModifier *= leadsInside ? 5 : 0; - } - else - { - // up to 100% priority increase for every gap in the wall - valueModifier *= 1 + section.gap.Open; - } - } - } - } - else + bool canAttackSub = Character.AnimController.CanAttackSubmarine; + if (!AggressiveBoarding) { // Ignore disabled walls bool isDisabled = true; @@ -1276,7 +1336,39 @@ namespace Barotrauma } if (isDisabled) { - valueModifier = 0; + continue; + } + } + //var hulls = s.Submarine.GetHulls(false); + for (int i = 0; i < s.Sections.Length; i++) + { + var section = s.Sections[i]; + if (section.gap != null) + { + if (AggressiveBoarding) + { + if (CanPassThroughHole(s, i)) + { + bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; // hulls.Any(h => h.Rect.Intersects(section.rect) + valueModifier *= leadsInside ? 5 : 0; + } + else + { + // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in if we are aggressive boarders + if (!canAttackSub) + { + valueModifier = 0; + break; + } + // Up to 100% priority increase for every gap in the wall + valueModifier *= 1 + section.gap.Open; + } + } + else + { + bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; + valueModifier *= leadsInside ? 1 : 0; + } } } } @@ -1293,17 +1385,24 @@ namespace Barotrauma } bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; bool isOpen = door.IsOpen || door.Item.Condition <= 0.0f; - //increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside - if (aggressiveBoarding) + if (!isOpen && (!Character.AnimController.CanAttackSubmarine)) { + // Ignore doors that are not open if cannot attack items/structures. Open doors should be targeted, so that we can get in if we are aggressive boarders + valueModifier = 0; + } + if (character.CurrentHull == null) + { + valueModifier = isOutdoor ? 1 : 0; + } + else if (AggressiveBoarding) + { + // Increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside if (character.CurrentHull == null) { - valueModifier = isOutdoor ? 1 : 0; valueModifier *= isOpen ? 5 : 1; } else { - valueModifier = isOutdoor ? 0 : 1; valueModifier *= isOpen ? 0 : 1; } } @@ -1319,9 +1418,9 @@ namespace Barotrauma } if (targetingTag == null) continue; - if (!targetingPriorities.ContainsKey(targetingTag)) continue; - - valueModifier *= targetingPriorities[targetingTag].Priority; + var targetPrio = GetTargetingPriority(targetingTag); + if (targetPrio == null) { continue; } + valueModifier *= targetPrio.Priority; if (valueModifier == 0.0f) continue; @@ -1333,7 +1432,7 @@ namespace Barotrauma if (targetMemories.ContainsKey(target)) dist *= 0.5f; //ignore target if it's too far to see or hear - if (dist > target.SightRange * sight && dist > target.SoundRange * hearing) continue; + if (dist > target.SightRange * Sight && dist > target.SoundRange * Hearing) continue; if (!target.IsWithinSector(WorldPosition)) continue; //if the target is very close, the distance doesn't make much difference @@ -1352,7 +1451,7 @@ namespace Barotrauma { newTarget = target; selectedTargetMemory = targetMemory; - priority = targetingPriorities[targetingTag]; + priority = GetTargetingPriority(targetingTag); targetValue = valueModifier; } } @@ -1392,6 +1491,15 @@ namespace Barotrauma removals.ForEach(r => targetMemories.Remove(r)); } + private const float targetIgnoreTime = 10; + private float targetIgnoreTimer; + private readonly HashSet ignoredTargets = new HashSet(); + public void IgnoreTarget(AITarget target) + { + ignoredTargets.Add(target); + targetIgnoreTimer = targetIgnoreTime; + } + #endregion protected override void OnStateChanged(AIState from, AIState to) @@ -1454,6 +1562,27 @@ namespace Barotrauma return holeCount >= requiredHoleCount; } + + private List targetLimbs = new List(); + public Limb GetTargetLimb(Limb attackLimb, LimbType targetLimbType, Character target) + { + targetLimbs.Clear(); + foreach (var limb in target.AnimController.Limbs) + { + if (limb.type == targetLimbType || targetLimbType == LimbType.None) + { + targetLimbs.Add(limb); + } + } + if (targetLimbs.None()) + { + // If no limbs of given type was found, accept any limb + targetLimbs.AddRange(target.AnimController.Limbs); + } + targetLimbs.Sort((limb1, limb2) => Vector2.DistanceSquared(limb1.WorldPosition, attackLimb.WorldPosition) + .CompareTo(Vector2.DistanceSquared(limb2.WorldPosition, attackLimb.WorldPosition))); + return targetLimbs.FirstOrDefault(); + } } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs index ec5cd01bb..117f28a91 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs @@ -69,6 +69,10 @@ namespace Barotrauma public HumanAIController(Character c) : base(c) { + if (!c.IsHuman) + { + throw new System.Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); + } insideSteering = new IndoorsSteeringManager(this, true, false); outsideSteering = new SteeringManager(this); objectiveManager = new AIObjectiveManager(c); @@ -324,7 +328,7 @@ namespace Barotrauma AddTargets(Character, c); if (newOrder == null) { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders"); + var orderPrefab = Order.GetPrefab("reportintruders"); newOrder = new Order(orderPrefab, c.CurrentHull, null, orderGiver: Character); } } @@ -334,7 +338,7 @@ namespace Barotrauma AddTargets(Character, hull); if (newOrder == null) { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire"); + var orderPrefab = Order.GetPrefab("reportfire"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); } } @@ -347,7 +351,7 @@ namespace Barotrauma { if (newOrder == null) { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid"); + var orderPrefab = Order.GetPrefab("requestfirstaid"); newOrder = new Order(orderPrefab, c.CurrentHull, null, orderGiver: Character); } } @@ -360,7 +364,7 @@ namespace Barotrauma AddTargets(Character, gap); if (newOrder == null && !gap.IsRoomToRoom) { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach"); + var orderPrefab = Order.GetPrefab("reportbreach"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); } } @@ -374,7 +378,7 @@ namespace Barotrauma AddTargets(Character, item); if (newOrder == null) { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbrokendevices"); + var orderPrefab = Order.GetPrefab("reportbrokendevices"); newOrder = new Order(orderPrefab, item.CurrentHull, item.Repairables?.FirstOrDefault(), orderGiver: Character); } } @@ -518,11 +522,7 @@ namespace Barotrauma } else if (ObjectiveManager.CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) { - //TODO: re-enable on all languages after DialogNoRescueTargets has been translated - if (TextManager.Language == "English") - { - Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); - } + Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); } else if (ObjectiveManager.CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) { @@ -620,7 +620,7 @@ namespace Barotrauma public static void RefreshTargets(Character character, Order order, Hull hull) { - switch (order.AITag) + switch (order.Identifier) { case "reportfire": AddTargets(character, hull); @@ -667,7 +667,7 @@ namespace Barotrauma break; default: #if DEBUG - DebugConsole.ThrowError(order.AITag + " not implemented!"); + DebugConsole.ThrowError(order.Identifier + " not implemented!"); #endif break; } @@ -765,6 +765,9 @@ namespace Barotrauma public bool IsFriendly(Character other) => IsFriendly(Character, other); - public static bool IsFriendly(Character me, Character other) => (other.TeamID == me.TeamID || other.TeamID == Character.TeamType.FriendlyNPC || me.TeamID == Character.TeamType.FriendlyNPC) && other.SpeciesName == me.SpeciesName; + public static bool IsFriendly(Character me, Character other) => + (other.TeamID == me.TeamID || + other.TeamID == Character.TeamType.FriendlyNPC || + me.TeamID == Character.TeamType.FriendlyNPC) && (other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group)); } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs index e68f9a3a2..5ddf4e7f3 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs @@ -130,7 +130,7 @@ namespace Barotrauma switch (enemyAI.State) { - case AIController.AIState.Idle: + case AIState.Idle: if (attachToWalls && character.Submarine == null && Level.Loaded != null) { raycastTimer -= deltaTime; @@ -187,7 +187,7 @@ namespace Barotrauma } } break; - case AIController.AIState.Attack: + case AIState.Attack: if (enemyAI.AttackingLimb != null) { if (attachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/NPCConversation.cs index 934817d00..89cbc856a 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/NPCConversation.cs @@ -33,7 +33,7 @@ namespace Barotrauma { if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } string language = doc.Root.GetAttributeString("Language", "English"); string identifier = doc.Root.GetAttributeString("Identifier", "unknown"); contentPackageFiles.Add(new Tuple(language, identifier, filePath)); @@ -44,7 +44,7 @@ namespace Barotrauma { if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) continue; + if (doc == null) { continue; } string language = doc.Root.GetAttributeString("Language", "English"); string identifier = doc.Root.GetAttributeString("Identifier", "unknown"); translationFiles.Add(new Tuple(language, identifier, filePath)); @@ -73,7 +73,7 @@ namespace Barotrauma private static void Load(string file) { XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null || doc.Root == null) return; + if (doc == null) { return; } string language = doc.Root.GetAttributeString("Language", "English"); if (language != TextManager.Language) return; @@ -102,8 +102,10 @@ namespace Barotrauma string allowedJobsStr = element.GetAttributeString("allowedjobs", ""); foreach (string allowedJobIdentifier in allowedJobsStr.Split(',')) { - var jobPrefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == allowedJobIdentifier.ToLowerInvariant()); - if (jobPrefab != null) AllowedJobs.Add(jobPrefab); + if (JobPrefab.List.TryGetValue(allowedJobIdentifier.ToLowerInvariant(), out JobPrefab jobPrefab)) + { + AllowedJobs.Add(jobPrefab); + } } Flags = new List(element.GetAttributeStringArray("flags", new string[0])); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs index 73c1614c7..f9ee68dff 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -115,7 +115,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager)); return; } - container.Combine(itemToContain); + container.Combine(itemToContain, character); } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs index 3ffaab0a8..671373dd4 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs @@ -68,13 +68,13 @@ namespace Barotrauma public void CreateAutonomousObjectives() { Objectives.Clear(); - AddObjective(new AIObjectiveFindSafety(character, this), delay: Rand.Value() / 2); - AddObjective(new AIObjectiveIdle(character, this), delay: Rand.Value() / 2); + AddObjective(new AIObjectiveFindSafety(character, this)); + AddObjective(new AIObjectiveIdle(character, this)); int objectiveCount = Objectives.Count; foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders) { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == automaticOrder.aiTag); - if (orderPrefab == null) { throw new Exception("Could not find a matching prefab by ai tag: " + automaticOrder.aiTag); } + var orderPrefab = Order.GetPrefab(automaticOrder.identifier); + if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); } // TODO: Similar code is used in CrewManager:815-> DRY var matchingItems = orderPrefab.ItemIdentifiers.Any() ? Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) : @@ -144,7 +144,7 @@ namespace Barotrauma if (previousObjective != CurrentObjective) { CurrentObjective?.OnSelected(); - GetObjective()?.SetRandom(); + GetObjective().SetRandom(); } return CurrentObjective; } @@ -231,7 +231,7 @@ namespace Barotrauma { if (order == null) { return null; } AIObjective newObjective; - switch (order.AITag.ToLowerInvariant()) + switch (order.Identifier.ToLowerInvariant()) { case "follow": if (orderGiver == null) { return null; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs index e520495bf..12ac3a962 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -53,7 +53,7 @@ namespace Barotrauma { if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > vitalityThreshold) { return false; } } - if (target.Submarine == null) { return false; } + if (target.Submarine == null || character.Submarine == null) { return false; } if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; } if (target.CurrentHull == null) { return false; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Order.cs index 5c7d6641c..a59c95c27 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Order.cs @@ -10,9 +10,16 @@ namespace Barotrauma { class Order { - private static string ConfigFile = Path.Combine("Content", "Orders.xml"); - - public static List PrefabList; + public static Dictionary Prefabs { 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 Order Prefab { @@ -27,7 +34,7 @@ namespace Barotrauma public readonly Type ItemComponentType; public readonly string[] ItemIdentifiers; - public readonly string AITag; + public readonly string Identifier; public readonly Color Color; @@ -43,30 +50,64 @@ namespace Barotrauma public Character OrderGiver; + //legacy support public readonly string[] AppropriateJobs; public readonly string[] Options; public readonly string[] OptionNames; static Order() { - PrefabList = new List(); + Prefabs = new Dictionary(); - XDocument doc = XMLExtensions.TryLoadXml(ConfigFile); - if (doc == null || doc.Root == null) return; - - foreach (XElement orderElement in doc.Root.Elements()) + foreach (string file in GameMain.Instance.GetFilesOfType(ContentType.Orders)) { - if (orderElement.Name.ToString().ToLowerInvariant() != "order") continue; - var newOrder = new Order(orderElement); - newOrder.Prefab = newOrder; - PrefabList.Add(newOrder); + XDocument doc = XMLExtensions.TryLoadXml(file); + 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 orderElement = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; + string name = orderElement.Name.ToString(); + if (name.Equals("order", StringComparison.OrdinalIgnoreCase)) + { + string identifier = orderElement.GetAttributeString("identifier", null); + if (string.IsNullOrWhiteSpace(identifier)) + { + DebugConsole.ThrowError($"Error in file {file}: 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}'", Color.Yellow); + Prefabs.Remove(identifier); + } + else + { + DebugConsole.ThrowError($"Error in file {file}: Duplicate element with the idenfitier '{identifier}' found in '{file}'! All orders must have a unique identifier. Use tags to override an order with the same identifier."); + continue; + } + } + var newOrder = new Order(orderElement); + newOrder.Prefab = newOrder; + Prefabs.Add(identifier, newOrder); + } + } } + PrefabList = new List(Prefabs.Values); } private Order(XElement orderElement) { - AITag = orderElement.GetAttributeString("aitag", ""); - Name = TextManager.Get("OrderName." + AITag, true) ?? "Name not found"; + Identifier = orderElement.GetAttributeString("identifier", ""); + Name = TextManager.Get("OrderName." + Identifier, true) ?? "Name not found"; string targetItemType = orderElement.GetAttributeString("targetitemtype", ""); if (!string.IsNullOrWhiteSpace(targetItemType)) @@ -78,7 +119,7 @@ namespace Barotrauma catch (Exception e) { - DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemType + " not found", e); + DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e); } } @@ -90,7 +131,7 @@ namespace Barotrauma AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); - string translatedOptionNames = TextManager.Get("OrderOptions." + AITag, true); + string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true); if (translatedOptionNames == null) { OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]); @@ -116,7 +157,7 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - SymbolSprite = new Sprite(subElement); + SymbolSprite = new Sprite(subElement, lazyLoad: true); break; } } @@ -127,7 +168,7 @@ namespace Barotrauma Prefab = prefab; Name = prefab.Name; - AITag = prefab.AITag; + Identifier = prefab.Identifier; ItemComponentType = prefab.ItemComponentType; Options = prefab.Options; SymbolSprite = prefab.SymbolSprite; @@ -155,8 +196,14 @@ namespace Barotrauma public bool HasAppropriateJob(Character character) { - if (AppropriateJobs == null || AppropriateJobs.Length == 0) return true; - if (character.Info == null || character.Info.Job == null) return false; + if (character.Info == null || character.Info.Job == null) { return false; } + if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; } + + if (!JobPrefab.List.Values.Any(jp => jp.AppropriateOrders.Contains(Identifier)) && + (AppropriateJobs == null || AppropriateJobs.Length == 0)) + { + return true; + } for (int i = 0; i < AppropriateJobs.Length; i++) { if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) return true; @@ -168,7 +215,7 @@ namespace Barotrauma { orderOption = orderOption ?? ""; - string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + AITag; + string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier; if (!string.IsNullOrEmpty(orderOption)) messageTag += "." + orderOption; if (targetCharacterName == null) targetCharacterName = ""; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/Source/Characters/AICharacter.cs index be8525a94..41812f4e9 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AICharacter.cs @@ -20,8 +20,8 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null) - : base(file, position, seed, characterInfo, isNetworkPlayer, ragdoll) + public AICharacter(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null) + : base(speciesName, position, seed, characterInfo, isNetworkPlayer, ragdoll) { InitProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs index 1ea079c97..4f3fb0a08 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs @@ -37,7 +37,7 @@ namespace Barotrauma } if (!CanWalk) { - DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!"); + //DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!"); return null; } else @@ -214,6 +214,8 @@ namespace Barotrauma return SwimSlowParams; case AnimationType.SwimFast: return SwimFastParams; + case AnimationType.NotDefined: + return null; default: throw new NotImplementedException(type.ToString()); } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs index 5a5b773b8..43040c7e2 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs @@ -285,16 +285,9 @@ namespace Barotrauma public override void DragCharacter(Character target, float deltaTime) { - if (target == null) return; - - Limb mouthLimb = Array.Find(Limbs, l => l != null && l.MouthPos.HasValue); - if (mouthLimb == null) mouthLimb = GetLimb(LimbType.Head); - - if (mouthLimb == null) - { - DebugConsole.ThrowError("Character \"" + character.SpeciesName + "\" failed to eat a target (a head or a limb with a mouthpos required)"); - return; - } + if (target == null) { return; } + Limb mouthLimb = GetLimb(LimbType.Head); + if (mouthLimb == null) { return; } if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -489,9 +482,9 @@ namespace Barotrauma { case LimbType.LeftFoot: case LimbType.RightFoot: - if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID)) + if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) { - SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.limbParams.ID] * Dir, MainLimb, FootTorque); + SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, MainLimb, FootTorque); } break; case LimbType.Tail: @@ -557,6 +550,9 @@ namespace Barotrauma movementAngle -= MathHelper.TwoPi; } + float stepLift = TargetMovement.X == 0.0f ? 0 : + (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + Limb torso = GetLimb(LimbType.Torso); if (torso != null) { @@ -566,7 +562,7 @@ namespace Barotrauma } if (TorsoPosition.HasValue) { - Vector2 pos = colliderBottom + Vector2.UnitY * TorsoPosition.Value; + Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift); if (torso != MainLimb) { @@ -588,7 +584,7 @@ namespace Barotrauma } if (HeadPosition.HasValue) { - Vector2 pos = colliderBottom + Vector2.UnitY * HeadPosition.Value; + Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); if (head != MainLimb) { @@ -673,10 +669,10 @@ namespace Barotrauma #if CLIENT if (playFootstepSound) { PlayImpactSound(limb); } #endif - if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID)) + if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) { SmoothRotateWithoutWrapping(limb, - movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.limbParams.ID] * Dir, + movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.Params.ID] * Dir, MainLimb, FootTorque); } break; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs index 532a6b480..e330c8e8d 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs @@ -637,9 +637,15 @@ namespace Barotrauma } else { - if (!onGround) movement = Vector2.Zero; + if (!onGround) + { + movement = Vector2.Zero; + } + + float stepLift = TargetMovement.X == 0.0f ? 0 : + (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); - float y = colliderPos.Y; + float y = colliderPos.Y + stepLift; if (TorsoPosition.HasValue) { y += TorsoPosition.Value; @@ -648,7 +654,7 @@ namespace Barotrauma MathUtils.SmoothStep(torso.SimPosition, new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce); - y = colliderPos.Y; + y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier; if (HeadPosition.HasValue) { y += HeadPosition.Value; @@ -809,10 +815,11 @@ namespace Barotrauma //get the elbow to a neutral rotation if (Math.Abs(hand.body.AngularVelocity) < 10.0f) { - LimbJoint elbow = - GetJointBetweenLimbs(armType, hand.type) ?? - GetJointBetweenLimbs(armType, foreArmType); - hand.body.ApplyTorque(MathHelper.Clamp(-elbow.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 10.0f); + LimbJoint elbow = GetJointBetweenLimbs(armType, hand.type) ?? GetJointBetweenLimbs(armType, foreArmType); + if (elbow != null) + { + hand.body.ApplyTorque(MathHelper.Clamp(-elbow.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 10.0f); + } } } } @@ -1848,7 +1855,11 @@ namespace Barotrauma } var torso = GetLimb(LimbType.Torso); var waist = GetJointBetweenLimbs(LimbType.Waist, upperLeg.type); - Vector2 waistPos = waist.LimbA == upperLeg ? waist.WorldAnchorA : waist.WorldAnchorB; + Vector2 waistPos = Vector2.Zero; + if (waist != null) + { + waistPos = waist.LimbA == upperLeg ? waist.WorldAnchorA : waist.WorldAnchorB; + } //distance from waist joint to the target position float c = Vector2.Distance(pos, waistPos); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Ragdoll/RagdollParams.cs deleted file mode 100644 index 06ab8d665..000000000 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Ragdoll/RagdollParams.cs +++ /dev/null @@ -1,676 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Xml.Linq; -using System.Linq; -using System.IO; -using Barotrauma.Extensions; -using System.Xml; - -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); - } - - class FishRagdollParams : RagdollParams - { - public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); - } - - class RagdollParams : EditableParams - { - public const float MIN_SCALE = 0.1f; - public const float MAX_SCALE = 2; - - public string SpeciesName { get; private set; } - - [Serialize(0f, true), Editable(-360, 360, ToolTip = "Rotation offset (in degrees) used for animations and widgets. If the sprites in the sheet are in different orientations, use the orientation of the torso for the final version of your character (while editing the character in the editor, you can change the orientation freely).")] - public float SpritesheetOrientation { get; set; } - - private float limbScale; - [Serialize(1.0f, true), 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)] - 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)] - public float TextureScale { get; set; } - - [Serialize(45f, true), Editable(0f, 1000f)] - public float ColliderHeightFromFloor { get; set; } - - [Serialize(50f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] - public float ImpactTolerance { get; set; } - - [Serialize(true, true), Editable] - public bool CanEnterSubmarine { get; set; } - - [Serialize(true, true), Editable] - public bool Draggable { get; set; } - - private static Dictionary> allRagdolls = new Dictionary>(); - - public List ColliderParams { get; private set; } = new List(); - public List Limbs { get; private set; } = new List(); - public List Joints { get; private set; } = new List(); - - protected IEnumerable GetAllSubParams() => - ColliderParams.Select(c => c as RagdollSubParams) - .Concat(Limbs.Select(j => j as RagdollSubParams) - .Concat(Joints.Select(j => j as RagdollSubParams))); - - 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"); - - private static readonly object[] dummyParams = new object[] - { - new XAttribute("type", "Dummy"), - new XElement("collider", new XAttribute("radius", 1)), - new XElement("limb", - new XAttribute("id", 0), - new XAttribute("type", LimbType.Head.ToString()), - new XAttribute("width", 1), - new XAttribute("height", 1), - new XElement("sprite", - new XAttribute("sourcerect", $"0, 0, 1, 1"))) - }; - - public static string GetFolder(string speciesName, ContentPackage contentPackage = null) - { - string configFilePath = Character.GetConfigFile(speciesName, contentPackage); - var folder = XMLExtensions.TryLoadXml(configFilePath)?.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty); - if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default") - { - folder = Path.Combine(Path.GetDirectoryName(configFilePath), "Ragdolls") + Path.DirectorySeparatorChar; - } - return folder; - } - - public static T GetDefaultRagdollParams(string 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() - { - if (!allRagdolls.TryGetValue(speciesName, out Dictionary ragdolls)) - { - ragdolls = new Dictionary(); - allRagdolls.Add(speciesName, ragdolls); - } - if (string.IsNullOrEmpty(fileName) || !ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) - { - string selectedFile = null; - string folder = GetFolder(speciesName); - if (Directory.Exists(folder)) - { - var files = Directory.GetFiles(folder); - 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).ToLowerInvariant() == fileName.ToLowerInvariant()); - if (selectedFile == null) - { - DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."); - selectedFile = GetDefaultFile(speciesName); - } - } - } - 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)) - { - ragdolls.Add(r.Name, r); - } - return r; - } - else - { - DebugConsole.ThrowError($"[RagdollParams] Failed to load ragdoll {r} at {selectedFile} for the character {speciesName}. Creating a dummy file."); - var defaultFile = GetDefaultFile(speciesName); - if (File.Exists(defaultFile)) - { - DebugConsole.ThrowError($"[RagdollParams] Renaming the invalid file as {selectedFile}.invalid"); - // Rename the old file so that it's not lost. - File.Move(defaultFile, defaultFile + ".invalid"); - } - return CreateDefault(defaultFile, speciesName, dummyParams); - } - } - return (T)ragdoll; - } - - /// - /// 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, params object[] ragdollConfig) where T : RagdollParams, new() - { - // Remove the old ragdolls, if found. - if (allRagdolls.ContainsKey(speciesName)) - { - DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red); - allRagdolls.Remove(speciesName); - } - var ragdolls = new Dictionary(); - allRagdolls.Add(speciesName, ragdolls); - var instance = new T(); - XElement ragdollElement = new XElement("Ragdoll", ragdollConfig); - instance.doc = new XDocument(ragdollElement); - instance.UpdatePath(fullPath); - instance.IsLoaded = instance.Deserialize(ragdollElement); - instance.Save(); - instance.Load(fullPath, speciesName); - ragdolls.Add(instance.Name, instance); - DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite); - return instance as T; - } - - protected override void UpdatePath(string fullPath) - { - if (SpeciesName == null) - { - base.UpdatePath(fullPath); - } - else - { - // Update the key by removing and re-adding the ragdoll. - if (allRagdolls.TryGetValue(SpeciesName, out Dictionary ragdolls)) - { - ragdolls.Remove(Name); - } - base.UpdatePath(fullPath); - if (ragdolls != null) - { - if (!ragdolls.ContainsKey(Name)) - { - ragdolls.Add(Name, this); - } - } - } - } - - public bool Save(string fileNameWithoutExtension = null) - { - OriginalElement = MainElement; - GetAllSubParams().ForEach(p => p.SetCurrentElementAsOriginalElement()); - Serialize(); - return base.Save(fileNameWithoutExtension, new XmlWriterSettings - { - Indent = true, - OmitXmlDeclaration = true, - NewLineOnAttributes = false - }); - } - - protected bool Load(string file, string speciesName) - { - if (Load(file)) - { - SpeciesName = speciesName; - CreateColliders(); - CreateLimbs(); - CreateJoints(); - return true; - } - return false; - } - - public override bool Reset(bool forceReload = false) - { - if (forceReload) - { - return Load(FullPath, SpeciesName); - } - Deserialize(OriginalElement, recursive: true); - GetAllSubParams().ForEach(sp => sp.Reset()); - return true; - } - - protected void CreateColliders() - { - ColliderParams.Clear(); - for (int i = 0; i < MainElement.Elements("collider").Count(); i++) - { - var element = MainElement.Elements("collider").ElementAt(i); - string name = i > 0 ? "Secondary Collider" : "Main Collider"; - ColliderParams.Add(new ColliderParams(element, this, name)); - } - } - - protected void CreateLimbs() - { - Limbs.Clear(); - foreach (var element in MainElement.Elements("limb")) - { - Limbs.Add(new LimbParams(element, this)); - } - Limbs = Limbs.OrderBy(l => l.ID).ToList(); - } - - protected void CreateJoints() - { - Joints.Clear(); - foreach (var element in MainElement.Elements("joint")) - { - Joints.Add(new JointParams(element, this)); - } - } - - protected bool Deserialize(XElement element = null, bool recursive = true) - { - if (base.Deserialize(element)) - { - if (recursive) - { - GetAllSubParams().ForEach(p => p.Deserialize()); - } - return true; - } - return false; - } - - protected bool Serialize(XElement element = null, bool recursive = true) - { - if (base.Serialize(element)) - { - if (recursive) - { - GetAllSubParams().ForEach(p => p.Serialize()); - } - return true; - } - return false; - } - - #region Memento - public override void CreateSnapshot() - { - Serialize(); - if (doc == null) - { - DebugConsole.ThrowError("[RagdollParams] The source XML Document is null!"); - return; - } - var copy = new RagdollParams - { - IsLoaded = true, - doc = new XDocument(doc) - }; - copy.CreateColliders(); - copy.CreateLimbs(); - copy.CreateJoints(); - copy.Deserialize(); - copy.Serialize(); - memento.Store(copy); - } - public override void Undo() => RevertTo(memento.Undo() as RagdollParams); - public override void Redo() => RevertTo(memento.Redo() as RagdollParams); - - private void RevertTo(RagdollParams source) - { - if (source.MainElement == null) - { - DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!"); - return; - } - Deserialize(source.MainElement, recursive: false); - var sourceSubParams = source.GetAllSubParams().ToList(); - var subParams = GetAllSubParams().ToList(); - // TODO: cannot currently undo joint/limb deletion. - if (sourceSubParams.Count != subParams.Count) - { - DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); - return; - } - for (int i = 0; i < subParams.Count; i++) - { - var subSubParams = subParams[i].SubParams; - if (subSubParams.Count != sourceSubParams[i].SubParams.Count) - { - DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); - return; - } - subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false); - for (int j = 0; j < subSubParams.Count; j++) - { - subSubParams[j].Deserialize(sourceSubParams[i].SubParams[j].Element, recursive: false); - // Since we cannot use recursion here, we have to go deeper manually, if necessary. - } - } - } - #endregion - -#if CLIENT - public void AddToEditor(ParamsEditor editor, bool alsoChildren = true) - { - base.AddToEditor(editor); - if (alsoChildren) - { - var subParams = GetAllSubParams(); - foreach (var subParam in subParams) - { - subParam.AddToEditor(editor); - new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, 10), editor.EditorBox.Content.RectTransform), - style: null, color: Color.Black); - } - } - } -#endif - } - - class JointParams : RagdollSubParams - { - public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } - - private string name; - [Serialize("", true), Editable] - public override string Name - { - get - { - if (string.IsNullOrWhiteSpace(name)) - { - name = GenerateName(); - } - return name; - } - set - { - name = value; - } - } - - public override string GenerateName() => $"Joint {Limb1} - {Limb2}"; - - [Serialize(-1, true), Editable] - public int Limb1 { get; set; } - - [Serialize(-1, true), Editable] - public int Limb2 { get; set; } - - /// - /// Should be converted to sim units. - /// - [Serialize("1.0, 1.0", true), Editable] - public Vector2 Limb1Anchor { get; set; } - - /// - /// Should be converted to sim units. - /// - [Serialize("1.0, 1.0", true), Editable] - public Vector2 Limb2Anchor { get; set; } - - [Serialize(true, true), Editable] - public bool CanBeSevered { get; set; } - - [Serialize(true, true), Editable] - public bool LimitEnabled { get; set; } - - /// - /// In degrees. - /// - [Serialize(0f, true), Editable] - public float UpperLimit { get; set; } - - /// - /// In degrees. - /// - [Serialize(0f, true), Editable] - public float LowerLimit { get; set; } - - [Serialize(0.25f, true), Editable] - public float Stiffness { get; set; } - } - - class LimbParams : RagdollSubParams - { - public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) - { - var spriteElement = element.Element("sprite"); - if (spriteElement != null) - { - normalSpriteParams = new SpriteParams(spriteElement, ragdoll); - SubParams.Add(normalSpriteParams); - } - var damagedElement = element.Element("damagedsprite"); - if (damagedElement != null) - { - damagedSpriteParams = new SpriteParams(damagedElement, ragdoll); - // Hide the damaged sprite params in the editor for now. - //SubParams.Add(damagedSpriteParams); - } - var deformElement = element.Element("deformablesprite"); - if (deformElement != null) - { - deformSpriteParams = new SpriteParams(deformElement, ragdoll); - SubParams.Add(deformSpriteParams); - } - } - - public readonly SpriteParams normalSpriteParams; - public readonly SpriteParams damagedSpriteParams; - public readonly SpriteParams deformSpriteParams; - - private string name; - [Serialize("", true), Editable] - public override string Name - { - get - { - if (string.IsNullOrWhiteSpace(name)) - { - name = GenerateName(); - } - return name; - } - set - { - name = value; - } - } - - public override string GenerateName() => $"Limb {ID}"; - - /// - /// Note that editing this in-game doesn't currently have any effect (unless the ragdoll is recreated). It should be visible, but readonly in the editor. - /// - [Serialize(-1, true), Editable] - public int ID { get; set; } - - [Serialize(LimbType.None, true), Editable] - public LimbType Type { get; set; } - - [Serialize(true, true), Editable] - public bool Flip { get; set; } - - [Serialize(0, true), Editable] - public int HealthIndex { get; set; } - - [Serialize(0f, true), Editable(ToolTip = "Higher values make AI characters prefer attacking this limb.")] - public float AttackPriority { get; set; } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] - public float SteerForce { get; set; } - - [Serialize("0, 0", true), Editable(ToolTip = "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.")] - public Vector2 StepOffset { get; set; } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] - public float Radius { get; set; } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] - public float Height { get; set; } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] - public float Width { get; set; } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10000)] - public float Mass { get; set; } - - [Serialize(10f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100)] - public float Density { get; set; } - - [Serialize("0, 0", true), Editable(ToolTip = "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot.")] - public Vector2 PullPos { get; set; } - - [Serialize(-1, true), Editable(ToolTip = "Only applicable if this limb is a foot. 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.")] - public int RefJoint { get; set; } - - [Serialize(false, true), Editable] - public bool IgnoreCollisions { get; set; } - - [Serialize("", true), Editable] - public string Notes { get; set; } - - // Non-editable -> - [Serialize(0.3f, true)] - public float Friction { get; set; } - - [Serialize(0.05f, true)] - public float Restitution { get; set; } - } - - class SpriteParams : RagdollSubParams - { - public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } - - [Serialize("0, 0, 0, 0", true), Editable] - public Rectangle SourceRect { get; set; } - - [Serialize("0.5, 0.5", true), Editable(DecimalCount = 2, ToolTip = "Relative to the collider.")] - public Vector2 Origin { get; set; } - - [Serialize(0f, true), Editable(DecimalCount = 3)] - public float Depth { get; set; } - - [Serialize("", true)] - public string Texture { get; set; } - } - - class ColliderParams : RagdollSubParams - { - public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) - { - Name = name; - } - - private string name; - [Serialize("", true), Editable] - public override string Name - { - get - { - if (string.IsNullOrWhiteSpace(name)) - { - name = GenerateName(); - } - return name; - } - set - { - name = value; - } - } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] - public float Radius { get; set; } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] - public float Height { get; set; } - - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] - public float Width { get; set; } - } - - abstract class RagdollSubParams : ISerializableEntity - { - public virtual string Name { get; set; } - public Dictionary SerializableProperties { get; private set; } - public XElement 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 RagdollSubParams(XElement element, RagdollParams ragdoll) - { - Element = element; - OriginalElement = new XElement(element); - Ragdoll = ragdoll; - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - } - - public virtual bool Deserialize(XElement element = null, bool recursive = true) - { - element = element ?? Element; - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - if (recursive) - { - SubParams.ForEach(sp => sp.Deserialize()); - } - return SerializableProperties != null; - } - - public virtual bool Serialize(XElement element = null, bool recursive = true) - { - element = element ?? Element; - SerializableProperty.SerializeProperties(this, element, true); - if (recursive) - { - SubParams.ForEach(sp => sp.Serialize()); - } - return true; - } - - public void SetCurrentElementAsOriginalElement() - { - OriginalElement = Element; - SubParams.ForEach(sp => sp.SetCurrentElementAsOriginalElement()); - } - - public void Reset() - { - Deserialize(OriginalElement, false); - SubParams.ForEach(sp => sp.Reset()); - } - - #if CLIENT - public SerializableEntityEditor SerializableEntityEditor { get; protected set; } - public virtual void AddToEditor(ParamsEditor editor) - { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true); - SubParams.ForEach(sp => sp.AddToEditor(editor)); - } - #endif - } -} diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs index c748180bf..7332c73b2 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using LimbParams = Barotrauma.RagdollParams.LimbParams; +using JointParams = Barotrauma.RagdollParams.JointParams; namespace Barotrauma { @@ -223,7 +225,7 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered || !limb.body.PhysEnabled) { continue; } limb.body.SetTransform(Collider.SimPosition, Collider.Rotation); //reset pull joints (they may be somewhere far away if the character has moved from the position where animations were last updated) limb.PullJointEnabled = false; @@ -233,8 +235,7 @@ namespace Barotrauma } } - // Currently the camera cannot handle greater speeds. It starts to lag behind. - public const float MAX_SPEED = 9; + public const float MAX_SPEED = 15; public Vector2 TargetMovement { @@ -258,6 +259,7 @@ namespace Barotrauma public float ImpactTolerance => RagdollParams.ImpactTolerance; public bool Draggable => RagdollParams.Draggable; public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; + public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure)); public float Dir { @@ -317,7 +319,7 @@ namespace Barotrauma } else { - items = limbs?.ToDictionary(l => l.limbParams, l => l.WearingItems); + items = limbs?.ToDictionary(l => l.Params, l => l.WearingItems); } foreach (var limbParams in RagdollParams.Limbs) { @@ -327,7 +329,7 @@ namespace Barotrauma limbParams.Radius = 10; } } - foreach (var colliderParams in RagdollParams.ColliderParams) + foreach (var colliderParams in RagdollParams.Colliders) { if (!PhysicsBody.IsValidShape(colliderParams.Radius, colliderParams.Height, colliderParams.Width)) { @@ -352,11 +354,16 @@ namespace Barotrauma limb.WearingItems.AddRange(itemList); } } - if (character.SpeciesName.ToLowerInvariant() == "humanhusk") + + if (character.IsHusk) { - if (Limbs.None(l => l.Name.ToLowerInvariant() == "huskappendage")) + if (Character.TryGetConfigFile(character.ConfigPath, out XDocument configFile)) { - AfflictionHusk.AttachHuskAppendage(character, this); + var mainElement = configFile.Root.IsOverride() ? configFile.Root.FirstElement() : configFile.Root; + foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) + { + AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this); + } } } } @@ -376,7 +383,7 @@ namespace Barotrauma } DebugConsole.Log($"Creating colliders from {RagdollParams.Name}."); collider = new List(); - foreach (ColliderParams cParams in RagdollParams.ColliderParams) + foreach (var cParams in RagdollParams.Colliders) { if (!PhysicsBody.IsValidShape(cParams.Radius, cParams.Height, cParams.Width)) { @@ -456,14 +463,12 @@ namespace Barotrauma /// public void SaveRagdoll(string fileNameWithoutExtension = null) { - SaveJoints(); - SaveLimbs(); RagdollParams.Save(fileNameWithoutExtension); } /// /// Resets the serializable data to the currently selected ragdoll params. - /// Force reloading always loads the xml stored in the disk. + /// Force reloading always loads the xml stored on the disk. /// public void ResetRagdoll(bool forceReload = false) { @@ -472,24 +477,6 @@ namespace Barotrauma ResetLimbs(); } - /// - /// Saves the current joint values to the serializable joint params. This method should properly handle character flipping. - /// NOTE: Currently all the params are handled stored as SubRagdollParams and handled in the RagdollParams Save method. This method does nothing. - /// - public void SaveJoints() - { - LimbJoints.ForEach(j => j.SaveParams()); - } - - /// - /// Handles custom serialization per limb. Currently only the attacks need to be serialized, since they cannot be stored as SubRagdollParams (because they shouldn't be decoupled with ragdolls). - /// Note: Saving to file is not handled by this method. Calling RagdollParams.Save() after this method should work. - /// - public void SaveLimbs() - { - Limbs.ForEach(l => l.attack?.Serialize()); - } - /// /// Resets the current joint values to the serialized joint params. /// @@ -792,17 +779,9 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered) continue; - + if (limb == null || limb.IsSevered) { continue; } limb.Dir = Dir; - - if (limb.MouthPos.HasValue) - { - limb.MouthPos = new Vector2( - -limb.MouthPos.Value.X, - limb.MouthPos.Value.Y); - } - + limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MirrorPullJoint(); } @@ -1052,6 +1031,21 @@ namespace Barotrauma /// private float bodyInRestTimer; + private float BodyInRestDelay = 1.0f; + + public bool BodyInRest + { + get { return bodyInRestTimer > BodyInRestDelay; } + set + { + foreach (Limb limb in Limbs) + { + limb.body.PhysEnabled = !value; + } + bodyInRestTimer = value ? BodyInRestDelay : 0.0f; + } + } + public bool forceStanding; public void Update(float deltaTime, Camera cam) @@ -1335,7 +1329,7 @@ namespace Barotrauma else if (Limbs.All(l => l != null && !l.body.Enabled || l.LinearVelocity.LengthSquared() < 0.001f)) { bodyInRestTimer += deltaTime; - if (bodyInRestTimer > 1.0f) + if (bodyInRestTimer > BodyInRestDelay) { foreach (Limb limb in Limbs) { @@ -1615,24 +1609,27 @@ namespace Barotrauma { if (GameMain.NetworkMember == null) return; - float lowestSubPos = ConvertUnits.ToSimUnits(Submarine.Loaded.Min(s => s.HiddenSubPosition.Y - s.Borders.Height - 128.0f)); - - for (int i = 0; i < character.MemState.Count; i++ ) + float lowestSubPos = float.MaxValue; + if (Submarine.Loaded.Any()) { - if (character.Submarine == null) + lowestSubPos = ConvertUnits.ToSimUnits(Submarine.Loaded.Min(s => s.HiddenSubPosition.Y - s.Borders.Height - 128.0f)); + for (int i = 0; i < character.MemState.Count; i++) { - //transform in-sub coordinates to outside coordinates - if (character.MemState[i].Position.Y > lowestSubPos) - character.MemState[i].TransformInToOutside(); - } - else if (currentHull?.Submarine != null) - { - //transform outside coordinates to in-sub coordinates - if (character.MemState[i].Position.Y < lowestSubPos) - character.MemState[i].TransformOutToInside(currentHull.Submarine); + if (character.Submarine == null) + { + //transform in-sub coordinates to outside coordinates + if (character.MemState[i].Position.Y > lowestSubPos) + character.MemState[i].TransformInToOutside(); + } + else if (currentHull?.Submarine != null) + { + //transform outside coordinates to in-sub coordinates + if (character.MemState[i].Position.Y < lowestSubPos) + character.MemState[i].TransformOutToInside(currentHull.Submarine); + } } } - + UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos); } @@ -1663,23 +1660,15 @@ namespace Barotrauma public Vector2? GetMouthPosition() { - Limb mouthLimb = Array.Find(Limbs, l => l != null && l.MouthPos.HasValue); - if (mouthLimb == null) mouthLimb = GetLimb(LimbType.Head); - if (mouthLimb == null) return null; - - Vector2 mouthPos = mouthLimb.SimPosition; - if (mouthLimb.MouthPos.HasValue) - { - float cos = (float)Math.Cos(mouthLimb.Rotation); - float sin = (float)Math.Sin(mouthLimb.Rotation); - mouthPos += new Vector2( - mouthLimb.MouthPos.Value.X * cos - mouthLimb.MouthPos.Value.Y * sin, - mouthLimb.MouthPos.Value.X * sin + mouthLimb.MouthPos.Value.Y * cos) * RagdollParams.LimbScale; - } - return mouthPos; + Limb mouthLimb = GetLimb(LimbType.Head); + if (mouthLimb == null) { return null; } + float cos = (float)Math.Cos(mouthLimb.Rotation); + float sin = (float)Math.Sin(mouthLimb.Rotation); + Vector2 bodySize = mouthLimb.body.GetSize(); + Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2); + return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * RagdollParams.LimbScale; } - public Vector2 GetColliderBottom() { float offset = 0.0f; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs b/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs index 287600bcc..57ecac8b2 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs @@ -30,7 +30,9 @@ namespace Barotrauma FallBack, FallBackUntilCanAttack, PursueIfCanAttack, - Pursue + Pursue, + FollowThrough, + FollowThroughUntilCanAttack } struct AttackResult @@ -65,42 +67,43 @@ namespace Barotrauma AppliedDamageModifiers = appliedDamageModifiers; } } - + partial class Attack : ISerializableEntity { - public readonly XElement SourceElement; - - [Serialize(AttackContext.NotDefined, true), Editable] + [Serialize(AttackContext.NotDefined, true, description: "Is the attack used only in a specific condition?"), Editable] public AttackContext Context { get; private set; } - [Serialize(AttackTarget.Any, true), Editable] + [Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable] public AttackTarget TargetType { get; private set; } - [Serialize(HitDetection.Distance, true), Editable] + [Serialize(LimbType.None, true, description: "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] public HitDetection HitDetectionType { get; private set; } - [Serialize(AIBehaviorAfterAttack.FallBack, true), Editable(ToolTip = "The preferred AI behavior after the attack.")] + [Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable] public AIBehaviorAfterAttack AfterAttack { get; set; } - [Serialize(false, true), Editable(ToolTip = "Should the ai try to reverse when aiming with this attack?")] + [Serialize(false, true, description: "Should the AI try to reverse when aiming with this attack?"), Editable] public bool Reverse { get; private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f, ToolTip = "Min distance from the attack limb to the target before the AI tries to attack.")] + [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)] public float Range { get; set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f, ToolTip = "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.")] + [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)] public float DamageRange { get; set; } - [Serialize(0.25f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ToolTip = "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.")] + [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)] public float Duration { get; private set; } - [Serialize(5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "How long the AI waits between the attacks.")] + [Serialize(5f, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0.")] + [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)] public float SecondaryCoolDown { get; set; } = 0; - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2, ToolTip = "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).")] + [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)] public float CoolDownRandomFactor { get; private set; } = 0; [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] @@ -115,7 +118,7 @@ namespace Barotrauma [Serialize(0.0f, false)] public float Stun { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, true, description: "Can damage only Humans."), Editable] public bool OnlyHumans { get; private set; } [Serialize("", true), Editable] @@ -139,36 +142,38 @@ namespace Barotrauma } } - [Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked.")] + [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)] public float Force { get; private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the attacking limb.")] + [Serialize(0.0f, true, description: "Applied to the attacking limb."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Torque { get; private set; } [Serialize(false, true), Editable] public bool ApplyForcesOnlyOnce { get; private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "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).")] + [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)] public float TargetImpulse { get; private set; } - [Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")] + [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable] public Vector2 TargetImpulseWorld { get; private set; } - [Serialize(0.0f, true), Editable(-1000.0f, 1000.0f, ToolTip = "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).")] + [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)] public float TargetForce { get; private set; } - [Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")] + [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable] public Vector2 TargetForceWorld { get; private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float SeverLimbsProbability { get; set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] - public float StickChance { get; set; } + // TODO: disabled because not synced + //[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + //public float StickChance { get; set; } + public float StickChance => 0f; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Priority { get; private set; } - + public IEnumerable StatusEffects { get { return statusEffects; } @@ -186,7 +191,7 @@ namespace Barotrauma //(if none, force is applied only to the limb the attack is attached to) public readonly List ForceOnLimbIndices = new List(); - public readonly List Afflictions = new List(); + public readonly Dictionary Afflictions = new Dictionary(); /// /// Only affects ai decision making. All the conditionals has to be met in order to select the attack. TODO: allow to define conditionals using any (implemented in StatusEffect -> move from there to PropertyConditional?) @@ -207,7 +212,7 @@ namespace Barotrauma public List GetMultipliedAfflictions(float multiplier) { List multipliedAfflictions = new List(); - foreach (Affliction affliction in Afflictions) + foreach (Affliction affliction in Afflictions.Keys) { multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source)); } @@ -227,7 +232,7 @@ namespace Barotrauma public float GetTotalDamage(bool includeStructureDamage = false) { float totalDamage = includeStructureDamage ? StructureDamage : 0.0f; - foreach (Affliction affliction in Afflictions) + foreach (Affliction affliction in Afflictions.Keys) { totalDamage += affliction.GetVitalityDecrease(null); } @@ -236,9 +241,9 @@ namespace Barotrauma public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float range = 0.0f) { - if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage)); - if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage)); - if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage)); + if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); + if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); + if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null); Range = range; DamageRange = range; @@ -247,8 +252,7 @@ namespace Barotrauma public Attack(XElement element, string parentDebugName) { - SourceElement = element; - Deserialize(); + Deserialize(element); if (element.Attribute("damage") != null || element.Attribute("bluntdamage") != null || @@ -258,8 +262,6 @@ namespace Barotrauma DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. )."); } - DamageRange = element.GetAttributeFloat("damagerange", 0f); - InitProjSpecific(element); foreach (XElement subElement in element.Elements()) @@ -297,10 +299,9 @@ namespace Barotrauma } } - float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); - var affliction = afflictionPrefab.Instantiate(afflictionStrength); - affliction.ApplyProbability = subElement.GetAttributeFloat("probability", 1.0f); - Afflictions.Add(affliction); + //float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); + //var affliction = afflictionPrefab.Instantiate(afflictionStrength); + //Afflictions.Add(affliction, subElement); break; case "conditional": @@ -310,21 +311,50 @@ namespace Barotrauma } break; } - } } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(XElement element = null); - public void Serialize() + public void ReloadAfflictions(XElement element) { - if (SourceElement == null) { return; } - SerializableProperty.SerializeProperties(this, SourceElement, true); + Afflictions.Clear(); + foreach (var subElement in element.GetChildElements("affliction")) + { + AfflictionPrefab afflictionPrefab; + Affliction affliction; + string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); + afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + if (afflictionPrefab != null) + { + float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); + affliction = afflictionPrefab.Instantiate(afflictionStrength); + } + else + { + affliction = new Affliction(null, 0); + } + affliction.Deserialize(subElement); + // add the affliction anyway, so that it can be shown in the editor. + Afflictions.Add(affliction, subElement); + } } - public void Deserialize() + public void Serialize(XElement element) { - if (SourceElement == null) { return; } - SerializableProperties = SerializableProperty.DeserializeProperties(this, SourceElement); + SerializableProperty.SerializeProperties(this, element, true); + foreach (var affliction in Afflictions) + { + if (affliction.Value != null) + { + affliction.Key.Serialize(affliction.Value); + } + } + } + + public void Deserialize(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + ReloadAfflictions(element); } public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true) @@ -332,7 +362,10 @@ namespace Barotrauma Character targetCharacter = target as Character; if (OnlyHumans) { - if (targetCharacter != null && targetCharacter.ConfigPath != Character.HumanConfigFile) return new AttackResult(); + if (targetCharacter != null && !targetCharacter.IsHuman) + { + return new AttackResult(); + } } SetUser(attacker); @@ -389,7 +422,10 @@ namespace Barotrauma if (OnlyHumans) { - if (targetLimb.character != null && targetLimb.character.ConfigPath != Character.HumanConfigFile) return new AttackResult(); + if (targetLimb.character != null && !targetLimb.character.IsHuman) + { + return new AttackResult(); + } } SetUser(attacker); @@ -418,7 +454,6 @@ namespace Barotrauma { effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast().ToList()); } - } return attackResult; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs index 9c1bb73fe..9e1e170d6 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs @@ -10,11 +10,13 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using FarseerPhysics.Dynamics; using Barotrauma.Extensions; +#if SERVER using System.Text; +#endif namespace Barotrauma { - partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable, ISpatialEntity + partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable { public static List CharacterList = new List(); @@ -92,7 +94,6 @@ namespace Barotrauma private Vector2 cursorPosition; - protected bool needsAir; protected float oxygenAvailable; //seed used to generate this character @@ -102,14 +103,33 @@ namespace Barotrauma public Character LastAttacker; public Entity LastDamageSource; - public readonly bool IsHumanoid; + public readonly CharacterParams Params; + public string SpeciesName => Params.SpeciesName; + public bool IsHumanoid => Params.Humanoid; + public bool IsHusk => Params.Husk; + + public bool CanSpeak + { + get => Params.CanSpeak; + set => Params.CanSpeak = value; + } + + public bool NeedsAir + { + get => Params.NeedsAir; + set => Params.NeedsAir = value; + } + + public float Noise + { + get => Params.Noise; + set => Params.Noise = value; + } public bool IsTraitor; public string TraitorCurrentObjective = ""; + public bool IsHuman => SpeciesName.Equals(HumanSpeciesName, StringComparison.OrdinalIgnoreCase); - //the name of the species (e.q. human) - public readonly string SpeciesName; - private float attackCoolDown; private Order currentOrder; @@ -206,11 +226,7 @@ namespace Barotrauma } } - public string ConfigPath - { - get; - private set; - } + public string ConfigPath => Params.File; public float Mass { @@ -325,8 +341,6 @@ namespace Barotrauma } } - private float Noise { get; set; } - private float pressureProtection; public float PressureProtection { @@ -347,12 +361,6 @@ namespace Barotrauma get { return CharacterHealth.IsUnconscious; } } - public bool NeedsAir - { - get { return needsAir; } - set { needsAir = value; } - } - public float Oxygen { get { return CharacterHealth.OxygenAmount; } @@ -392,6 +400,8 @@ namespace Barotrauma get { return CharacterHealth.Vitality; } } + public float HealthPercentage => CharacterHealth.HealthPercentage; + public float MaxVitality { get { return CharacterHealth.MaxVitality; } @@ -411,29 +421,6 @@ namespace Barotrauma { get { return CharacterHealth.GetAfflictionStrength("bleeding", true); } } - - public float HuskInfectionState - { - get - { - var huskAffliction = CharacterHealth.GetAffliction("huskinfection", false) as AfflictionHusk; - return huskAffliction == null ? 0.0f : huskAffliction.Strength; - } - set - { - var huskAffliction = CharacterHealth.GetAffliction("huskinfection", false) as AfflictionHusk; - if (huskAffliction == null) - { - CharacterHealth.ApplyAffliction(null, AfflictionPrefab.Husk.Instantiate(value)); - } - else - { - huskAffliction.Strength = value; - } - } - } - - public bool CanSpeak; private bool speechImpedimentSet; @@ -544,7 +531,7 @@ namespace Barotrauma { if (AnimController?.Collider == null) { - string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed+"."; + string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + "."; if (AnimController == null) { errorMsg += " AnimController == null"; @@ -598,13 +585,13 @@ namespace Barotrauma /// Ragdoll configuration file. If null, will select the default. public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null) { - return Create(characterInfo.File, position, seed, characterInfo, isRemotePlayer, hasAi, true, ragdoll); + return Create(characterInfo.SpeciesName, position, seed, characterInfo, isRemotePlayer, hasAi, true, ragdoll); } /// /// Create a new character /// - /// The path to the character's config file. + /// Name of the species (or the path to the config file) /// Position in display units. /// RNG seed to use if the character config has randomizable parameters. /// The name, gender, etc of the character. Only used for humans, and if the parameter is not given, a random CharacterInfo is generated. @@ -612,59 +599,58 @@ namespace Barotrauma /// Is the character controlled by AI. /// Should clients receive a network event about the creation of this character? /// Ragdoll configuration file. If null, will select the default. - public static Character Create(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) { -#if LINUX - if (!System.IO.File.Exists(file)) + if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { - //if the file was not found, attempt to convert the name of the folder to upper case - var splitPath = file.Split('/'); - if (splitPath.Length > 2) - { - splitPath[splitPath.Length-2] = - splitPath[splitPath.Length-2].First().ToString().ToUpper() + splitPath[splitPath.Length-2].Substring(1); - - file = string.Join("/", splitPath); - } - - if (!System.IO.File.Exists(file)) - { - DebugConsole.ThrowError("Spawning a character failed - file \""+file+"\" not found!"); - return null; - } + speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); } -#else - if (!System.IO.File.Exists(file)) - { - DebugConsole.ThrowError("Spawning a character failed - file \"" + file + "\" not found!"); - return null; - } -#endif Character newCharacter = null; - if (file != HumanConfigFile) + if (!speciesName.Equals(HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { - var aiCharacter = new AICharacter(file, position, seed, characterInfo, isRemotePlayer, ragdoll); - var ai = new EnemyAIController(aiCharacter, file, seed); + var aiCharacter = new AICharacter(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); + var ai = new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); - - //aiCharacter.minVitality = 0.0f; - newCharacter = aiCharacter; } else if (hasAi) { - var aiCharacter = new AICharacter(file, position, seed, characterInfo, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); var ai = new HumanAIController(aiCharacter); aiCharacter.SetAI(ai); - - //aiCharacter.minVitality = -100.0f; - newCharacter = aiCharacter; } else { - newCharacter = new Character(file, position, seed, characterInfo, isRemotePlayer, ragdoll); - //newCharacter.minVitality = -100.0f; + newCharacter = new Character(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); + } + + float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration; + if (healthRegen > 0) + { + AddDamageReduction("damage", healthRegen); + } + float eatingRegen = newCharacter.Params.Health.HealthRegenerationWhenEating; + if (eatingRegen > 0) + { + AddDamageReduction("damage", eatingRegen, ActionType.OnEating); + } + float burnReduction = newCharacter.Params.Health.BurnReduction; + if (burnReduction > 0) + { + AddDamageReduction("burn", burnReduction); + } + float bleedReduction = newCharacter.Params.Health.BleedingReduction; + if (bleedReduction > 0) + { + AddDamageReduction("bleeding", bleedReduction); + } + + 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})")); } #if SERVER @@ -676,30 +662,35 @@ namespace Barotrauma return newCharacter; } - protected Character(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, RagdollParams ragdollParams = null) + protected Character(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, RagdollParams ragdollParams = null) : base(null) { - ConfigPath = file; + string path = GetConfigFilePath(speciesName); + if (!TryGetConfigFile(path, out XDocument doc)) + { + throw new Exception($"Failed to load the config file for {speciesName} from {path}!"); + } this.seed = seed; MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); selectedItems = new Item[2]; IsRemotePlayer = isRemotePlayer; - + oxygenAvailable = 100.0f; aiTarget = new AITarget(this); lowPassMultiplier = 1.0f; Properties = SerializableProperty.GetProperties(this); + Params = new CharacterParams(path); Info = characterInfo; - if (file == HumanConfigFile || file == GetConfigFile("humanhusk")) + if (speciesName.Equals(HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { if (characterInfo == null) { - Info = new CharacterInfo(HumanConfigFile); + Info = new CharacterInfo(HumanSpeciesName); } } @@ -709,48 +700,16 @@ namespace Barotrauma keys[i] = new Key((InputType)i); } - XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null || doc.Root == null) return; - - InitProjSpecific(doc); - SpeciesName = doc.Root.GetAttributeString("name", "Unknown"); - displayName = TextManager.Get($"Character.{Path.GetFileName(Path.GetDirectoryName(file))}", true); - - IsHumanoid = doc.Root.GetAttributeBool("humanoid", false); - CanSpeak = doc.Root.GetAttributeBool("canspeak", false); - needsAir = doc.Root.GetAttributeBool("needsair", false); - Noise = doc.Root.GetAttributeFloat("noise", 100f); - - //List ragdollElements = new List(); - //List ragdollCommonness = new List(); - //foreach (XElement element in doc.Root.Elements()) - //{ - // if (element.Name.ToString().ToLowerInvariant() != "ragdoll") continue; - // ragdollElements.Add(element); - // ragdollCommonness.Add(element.GetAttributeFloat("commonness", 1.0f)); - //} - - ////choose a random ragdoll element - //XElement ragdollElement = ragdollElements.Count == 1 ? - // ragdollElements[0] : ToolBox.SelectWeightedRandom(ragdollElements, ragdollCommonness, random); - - if (IsHumanoid) - { - AnimController = new HumanoidAnimController(this, seed, ragdollParams as HumanRagdollParams); - AnimController.TargetDir = Direction.Right; - - } - else - { - AnimController = new FishAnimController(this, seed, ragdollParams as FishRagdollParams); - PressureProtection = 100.0f; - } + var rootElement = doc.Root; + var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; + InitProjSpecific(mainElement); + displayName = TextManager.Get($"Character.{speciesName}", true); List inventoryElements = new List(); List inventoryCommonness = new List(); List healthElements = new List(); List healthCommonness = new List(); - foreach (XElement subElement in doc.Root.Elements()) + foreach (XElement subElement in mainElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -767,10 +726,11 @@ namespace Barotrauma break; } } + if (inventoryElements.Count > 0) { Inventory = new CharacterInventory( - inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random), + inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random), this); } if (healthElements.Count == 0) @@ -780,10 +740,42 @@ namespace Barotrauma else { CharacterHealth = new CharacterHealth( - healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random), + healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random), this); } + if (Params.Husk) + { + // Get the non husked name and find the ragdoll with it + var matchingAffliction = AfflictionPrefab.List + .Where(p => p.AfflictionType == "huskinfection") + .Select(p => p as AfflictionPrefabHusk) + .FirstOrDefault(p => p.TargetSpecies.Contains(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p))); + 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!"); + return; + } + string nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction); + ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(nonHuskedSpeciesName) : RagdollParams.GetDefaultRagdollParams(nonHuskedSpeciesName) as RagdollParams; + if (info == null) + { + info = new CharacterInfo(nonHuskedSpeciesName, ragdollParams.FileName); + } + } + + if (IsHumanoid) + { + AnimController = new HumanoidAnimController(this, seed, ragdollParams as HumanRagdollParams); + AnimController.TargetDir = Direction.Right; + + } + else + { + AnimController = new FishAnimController(this, seed, ragdollParams as FishRagdollParams); + PressureProtection = 100.0f; + } + AnimController.SetPosition(ConvertUnits.ToSimUnits(position)); AnimController.FindHull(null); @@ -798,12 +790,12 @@ namespace Barotrauma // - if an AICharacter, the server enables it when close enough to any of the players Enabled = GameMain.NetworkMember == null; - if (Info != null) + if (info != null) { LoadHeadAttachments(); } } - partial void InitProjSpecific(XDocument doc); + partial void InitProjSpecific(XElement mainElement); public void ReloadHead(int? headId = null, int hairIndex = -1, int beardIndex = -1, int moustacheIndex = -1, int faceAttachmentIndex = -1) { @@ -812,7 +804,7 @@ namespace Barotrauma if (head == null) { return; } Info.RecreateHead(headId ?? Info.HeadSpriteId, Info.Race, Info.Gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); #if CLIENT - head.RecreateSprite(); + head.RecreateSprites(); #endif LoadHeadAttachments(); } @@ -843,67 +835,137 @@ namespace Barotrauma #endif } - private static string humanConfigFile; - public static string HumanConfigFile - { - get - { - if (string.IsNullOrEmpty(humanConfigFile)) - { - humanConfigFile = GameMain.Instance.GetFilesOfType(ContentType.Character)? - .FirstOrDefault(c => Path.GetFileName(c).ToLowerInvariant() == "human.xml"); - - if (humanConfigFile == null) - { - DebugConsole.ThrowError($"Couldn't find a human config file from the selected content packages!"); - DebugConsole.ThrowError($"(The config file must end with \"human.xml\")"); - return string.Empty; - } - } - return humanConfigFile; - } - } - - private static IEnumerable characterConfigFiles; - private static IEnumerable CharacterConfigFiles - { - get - { - if (characterConfigFiles == null) - { - characterConfigFiles = GameMain.Instance.GetFilesOfType(ContentType.Character); - } - return characterConfigFiles; - } - } + public static string HumanSpeciesName = "human"; + public static string HumanConfigFile => GetConfigFilePath(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 string GetConfigFile(string speciesName, ContentPackage contentPackage = null) + public static string GetConfigFilePath(string speciesName, ContentPackage contentPackage = null) { - string configFile = null; - if (contentPackage == null) + if (configFilePaths.None() || configFiles.None()) { - configFile = GameMain.Instance.GetFilesOfType(ContentType.Character) - .FirstOrDefault(c => Path.GetFileName(c).ToLowerInvariant() == $"{speciesName.ToLowerInvariant()}.xml"); + LoadAllConfigFiles(); } - else + string configFile = null; + if (contentPackage != null) { configFile = contentPackage.GetFilesOfType(ContentType.Character)? .FirstOrDefault(c => Path.GetFileName(c).ToLowerInvariant() == $"{speciesName.ToLowerInvariant()}.xml"); - } - if (configFile == null) + if (configFile == null) + { + DebugConsole.ThrowError($"Couldn't find a config file for {speciesName} from the specified content package {contentPackage.Name} defined in {contentPackage.Path}!"); + DebugConsole.ThrowError($"(The config file must end with \"{speciesName}.xml\")"); + return string.Empty; + } + } + else { - DebugConsole.ThrowError($"Couldn't find a config file for {speciesName} from the selected content packages!"); - DebugConsole.ThrowError($"(The config file must end with \"{speciesName}.xml\")"); - return string.Empty; + if (!configFilePaths.TryGetValue(speciesName.ToLowerInvariant(), out configFile)) + { + DebugConsole.ThrowError($"Couldn't find a config file for species \"{speciesName}\" from the selected content packages!"); + } } return configFile; } + private readonly static Dictionary configFilePaths = new Dictionary(); + private readonly static Dictionary configFiles = new Dictionary(); + + public static IEnumerable ConfigFilePaths => configFiles.Keys; + public static IEnumerable ConfigFiles => configFiles.Values; + + public static bool TryAddConfigFile(string file, bool forceOverride) + { + if (configFilePaths.None() || configFiles.None()) + { + LoadAllConfigFiles(); + } + return AddConfigFile(file, forceOverride); + } + + private static bool AddConfigFile(string file, bool forceOverride = false) + { + XDocument doc = XMLExtensions.TryLoadXml(file); + if (doc == null) + { + DebugConsole.ThrowError($"Loading character file failed: {file}"); + return false; + } + if (configFilePaths.ContainsKey(file)) + { + DebugConsole.ThrowError($"Duplicate path: {file}"); + return false; + } + XElement mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + var name = mainElement.GetAttributeString("name", null); + if (name != null) + { + DebugConsole.NewMessage($"Error in {file}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); + } + else + { + name = mainElement.GetAttributeString("speciesname", string.Empty); + } + if (string.IsNullOrWhiteSpace(name)) + { + DebugConsole.ThrowError($"No species name defined for: {file}"); + return false; + } + name = name.ToLowerInvariant(); + var duplicate = configFiles.FirstOrDefault(kvp => (kvp.Value.Root.IsOverride() ? kvp.Value.Root.FirstElement() : kvp.Value.Root) + .GetAttributeString("speciesname", string.Empty).Equals(name, StringComparison.OrdinalIgnoreCase)); + if (duplicate.Value != null) + { + if (forceOverride || doc.Root.IsOverride()) + { + DebugConsole.NewMessage($"Overriding the existing character '{name}' defined in '{duplicate.Key}' with '{file}'", Color.Yellow); + configFiles.Remove(duplicate.Key); + configFilePaths.Remove(name); + } + else + { + DebugConsole.ThrowError($"Duplicate species name '{name}' in '{file}'! Add tags as the parent of the character definition to override an existing character."); + return false; + } + + } + configFiles.Add(file, doc); + configFilePaths.Add(name, file); + return true; + } + + public static XDocument GetConfigFile(string speciesName) + { + string file = GetConfigFilePath(speciesName); + if (!TryGetConfigFile(file, out XDocument doc)) + { + DebugConsole.ThrowError($"Failed to load the config file for {speciesName} from {file}!"); + } + return doc; + } + + public static bool TryGetConfigFile(string file, out XDocument doc) + { + doc = null; + if (configFiles.None()) { LoadAllConfigFiles(); } + if (string.IsNullOrWhiteSpace(file)) { return false; } + configFiles.TryGetValue(file, out doc); + return doc != null; + } + + public static void LoadAllConfigFiles() + { + configFiles.Clear(); + configFilePaths.Clear(); + foreach (var file in ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Character)) + { + AddConfigFile(file); + } + } + public bool IsKeyHit(InputType inputType) { #if SERVER @@ -1020,7 +1082,7 @@ namespace Barotrauma public void ClearInput(InputType inputType) { keys[(int)inputType].Hit = false; - keys[(int)inputType].Held = false; + keys[(int)inputType].Held = false; } public void ClearInputs() @@ -1049,7 +1111,7 @@ namespace Barotrauma { return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier); } - + // TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner public Vector2? OverrideMovement { get; set; } public bool ForceRun { get; set; } @@ -1090,7 +1152,7 @@ namespace Barotrauma (!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) && !AnimController.IsMovingBackwards; } - + float currentSpeed = AnimController.GetCurrentSpeed(run); targetMovement *= currentSpeed; float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed); @@ -1189,7 +1251,7 @@ namespace Barotrauma smoothedCursorDiff = NetConfig.InterpolateCursorPositionError(smoothedCursorDiff); SmoothedCursorPosition = cursorPosition - smoothedCursorDiff; } - + if (!(this is AICharacter) || Controlled == this || IsRemotePlayer) { Vector2 targetMovement = GetTargetMovement(); @@ -1200,7 +1262,7 @@ namespace Barotrauma if (AnimController is HumanoidAnimController) { - ((HumanoidAnimController) AnimController).Crouching = IsKeyDown(InputType.Crouch); + ((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch); } if (AnimController.onGround && @@ -1225,7 +1287,7 @@ namespace Barotrauma AnimController.TargetDir = Direction.Right; } } - + if (GameMain.NetworkMember != null) { if (GameMain.NetworkMember.IsServer) @@ -1310,8 +1372,8 @@ namespace Barotrauma } else if (body.UserData is Limb) { - attackTarget = ((Limb)body.UserData).character; - } + attackTarget = ((Limb)body.UserData).character; + } } } @@ -1326,7 +1388,7 @@ namespace Barotrauma if (SelectedConstruction == null || !SelectedConstruction.Prefab.DisableItemUsageWhenSelected) { - for (int i = 0; i < selectedItems.Length; i++ ) + for (int i = 0; i < selectedItems.Length; i++) { if (selectedItems[i] == null) { continue; } if (i == 1 && selectedItems[0] == selectedItems[1]) { continue; } @@ -1352,7 +1414,7 @@ namespace Barotrauma } } } - + if (SelectedConstruction != null) { if (IsKeyDown(InputType.Aim) || !SelectedConstruction.RequireAimToSecondaryUse) @@ -1382,8 +1444,8 @@ namespace Barotrauma DeselectCharacter(); } } - - if (IsRemotePlayer && keys!=null) + + if (IsRemotePlayer && keys != null) { foreach (Key key in keys) { @@ -1521,12 +1583,12 @@ namespace Barotrauma bool leftHand = Inventory.IsInLimbSlot(item, InvSlotType.LeftHand); bool selected = false; - if (rightHand && (SelectedItems[0] == null || SelectedItems[0] == item)) + if (rightHand && (selectedItems[0] == null || selectedItems[0] == item)) { selectedItems[0] = item; selected = true; } - if (leftHand && (SelectedItems[1] == null || SelectedItems[1] == item)) + if (leftHand && (selectedItems[1] == null || selectedItems[1] == item)) { selectedItems[1] = item; selected = true; @@ -1584,7 +1646,7 @@ namespace Barotrauma return checkVisibility ? CanSeeCharacter(c) : true; } - + public bool CanInteractWith(Item item) { return CanInteractWith(item, out float distanceToItem, checkLinked: true); @@ -1637,12 +1699,12 @@ namespace Barotrauma } } } - + if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; } - + Pickable pickableComponent = item.GetComponent(); if (pickableComponent != null && (pickableComponent.Picker != null && !pickableComponent.Picker.IsDead)) { return false; } - + Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation)); Vector2 upperBodyPosition = Position + (characterDirection * 20.0f); @@ -1661,7 +1723,7 @@ namespace Barotrauma // Get the point along the line between lowerBodyPosition and upperBodyPosition which is closest to the center of itemDisplayRect Vector2 playerDistanceCheckPosition = Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition); - + // If playerDistanceCheckPosition is inside the itemDisplayRect then we consider the character to within 0 distance of the item if (itemDisplayRect.Contains(playerDistanceCheckPosition)) { @@ -1739,7 +1801,7 @@ namespace Barotrauma public void SelectCharacter(Character character) { if (character == null) return; - + SelectedCharacter = character; } @@ -1780,7 +1842,7 @@ namespace Barotrauma #if CLIENT if (isLocalPlayer) { - if (GUI.MouseOn == null && + if (GUI.MouseOn == null && (!CharacterInventory.IsMouseOnInventory() || CharacterInventory.DraggingItemToWorld)) { if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) @@ -1793,14 +1855,14 @@ namespace Barotrauma } else { - focusedItem = null; + focusedItem = null; } findFocusedTimer -= deltaTime; } #endif //climb ladders automatically when pressing up/down inside their trigger area Ladder currentLadder = SelectedConstruction?.GetComponent(); - if ((SelectedConstruction == null || currentLadder != null) && + if ((SelectedConstruction == null || currentLadder != null) && !AnimController.InWater && Screen.Selected != GameMain.SubEditorScreen) { bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down); @@ -1840,7 +1902,7 @@ namespace Barotrauma if (nearbyLadder.Select(this)) SelectedConstruction = nearbyLadder.Item; } } - + if (SelectedCharacter != null && (IsKeyHit(InputType.Grab) || IsKeyHit(InputType.Health))) //Let people use ladders and buttons and stuff when dragging chars { DeselectCharacter(); @@ -1895,13 +1957,13 @@ namespace Barotrauma #endif } } - + public static void UpdateAnimAll(float deltaTime) { foreach (Character c in CharacterList) { if (!c.Enabled || c.AnimController.Frozen) continue; - + c.AnimController.UpdateAnim(deltaTime); } } @@ -1993,10 +2055,10 @@ namespace Barotrauma public virtual void Update(float deltaTime, Camera cam) { UpdateProjSpecific(deltaTime, cam); - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) return; - if (!Enabled) return; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; } + + if (!Enabled) { return; } if (Level.Loaded != null && WorldPosition.Y < Level.MaxEntityDepth || (Submarine != null && Submarine.WorldPosition.Y < Level.MaxEntityDepth)) @@ -2019,7 +2081,7 @@ namespace Barotrauma { foreach (Item item in Inventory.Items) { - if (item == null || item.body == null || item.body.Enabled) continue; + if (item == null || item.body == null || item.body.Enabled) { continue; } item.SetTransform(SimPosition, 0.0f); item.Submarine = Submarine; @@ -2028,8 +2090,12 @@ namespace Barotrauma HideFace = false; - if (IsDead) return; - + + UpdateSightRange(); + UpdateSoundRange(); + + if (IsDead) { return; } + if (GameMain.NetworkMember != null) { UpdateNetInput(); @@ -2047,10 +2113,10 @@ namespace Barotrauma speechImpediment = 0.0f; } speechImpedimentSet = false; - - if (needsAir) + + if (NeedsAir) { - bool protectedFromPressure = PressureProtection > 0.0f; + bool protectedFromPressure = PressureProtection > 0.0f; //cannot be protected from pressure when below crush depth protectedFromPressure = protectedFromPressure && WorldPosition.Y > CharacterHealth.CrushDepth; //implode if not protected from pressure, and either outside or in a high-pressure hull @@ -2092,14 +2158,14 @@ namespace Barotrauma } } - ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); + ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); UpdateControlled(deltaTime, cam); - + //Health effects - if (needsAir) UpdateOxygen(deltaTime); + if (NeedsAir) UpdateOxygen(deltaTime); CharacterHealth.Update(deltaTime); - + if (IsUnconscious) { UpdateUnconscious(deltaTime); @@ -2136,13 +2202,10 @@ namespace Barotrauma IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.25f; } } - } - - UpdateSightRange(); - UpdateSoundRange(); + } lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); - + //ragdoll button if (IsRagdolled) { @@ -2165,12 +2228,12 @@ namespace Barotrauma Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition); DoInteractionUpdate(deltaTime, mouseSimPos); } - + if (SelectedConstruction != null && !CanInteractWith(SelectedConstruction)) { SelectedConstruction = null; } - + if (!IsDead) LockHands = false; } @@ -2224,7 +2287,7 @@ namespace Barotrauma if (orderGiver != null) { //set the character order only if the character is close enough to hear the message - ChatMessageType messageType = ChatMessage.CanUseRadio(orderGiver) && ChatMessage.CanUseRadio(this) ? + ChatMessageType messageType = ChatMessage.CanUseRadio(orderGiver) && ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default; if (string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, orderGiver, this))) return; } @@ -2351,12 +2414,18 @@ namespace Barotrauma float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime; var attackResult = targetLimb == null ? - AddDamage(worldPosition, attack.Afflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker) : - DamageLimb(worldPosition, targetLimb, attack.Afflictions, attack.Stun, playSound, attackImpulse, attacker); + AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker) : + DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker); + + if (limbHit == null) { return new AttackResult(); } - if (limbHit == null) return new AttackResult(); - limbHit.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + var mainLimb = limbHit.character.AnimController.MainLimb; + if (limbHit != mainLimb) + { + // Always add force to mainlimb + mainLimb.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } #if SERVER if (attacker is Character attackingCharacter && attackingCharacter.AIController == null) { @@ -2376,43 +2445,46 @@ namespace Barotrauma bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; + TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability); + + return attackResult; + } + + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability) + { + bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; + if (isNotClient && - IsDead && Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability) + IsDead && Rand.Range(0.0f, 1.0f) < severLimbsProbability) { foreach (LimbJoint joint in AnimController.LimbJoints) { - if (joint.CanBeSevered && (joint.LimbA == limbHit || joint.LimbB == limbHit)) + if (joint.CanBeSevered && (joint.LimbA == targetLimb || joint.LimbB == targetLimb)) { #if CLIENT - if (CurrentHull != null) - { - CurrentHull.AddDecal("blood", WorldPosition, Rand.Range(0.5f, 1.5f)); - } + CurrentHull?.AddDecal("blood", WorldPosition, Rand.Range(0.5f, 1.5f)); #endif - AnimController.SeverLimbJoint(joint); - if (joint.LimbA == limbHit) + if (joint.LimbA == targetLimb) { - joint.LimbB.body.LinearVelocity += limbHit.LinearVelocity * 0.5f; + joint.LimbB.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; } else { - joint.LimbA.body.LinearVelocity += limbHit.LinearVelocity * 0.5f; + joint.LimbA.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; } } } } - - return attackResult; } - - public AttackResult AddDamage(Vector2 worldPosition, List afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null) + + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null) { return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker); } - public AttackResult AddDamage(Vector2 worldPosition, List afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null) { hitLimb = null; @@ -2437,10 +2509,24 @@ namespace Barotrauma return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker); } - public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, List afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null) + public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null) { if (Removed) { return new AttackResult(); } + //character inside the sub received damage from a monster outside the sub + //can happen during normal gameplay if someone for example fires a ranged weapon from outside, + //the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside + if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null) + { + string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})"; + GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name, + GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, + errorMsg + "\n" + Environment.StackTrace); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#endif + } + if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) { if (attacker.TeamID == TeamID) { return new AttackResult(); } @@ -2451,9 +2537,16 @@ namespace Barotrauma if (Math.Abs(attackImpulse) > 0.0f) { Vector2 diff = dir; - if (diff == Vector2.Zero) diff = Rand.Vector(1.0f); - hitLimb.body.ApplyLinearImpulse(Vector2.Normalize(diff) * attackImpulse, hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff), - maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + if (diff == Vector2.Zero) { diff = Rand.Vector(1.0f); } + Vector2 impulse = Vector2.Normalize(diff) * attackImpulse; + Vector2 hitPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff); + hitLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); + var mainLimb = hitLimb.character.AnimController.MainLimb; + if (hitLimb != mainLimb) + { + // Always add force to mainlimb + mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } } Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound); @@ -2476,12 +2569,13 @@ namespace Barotrauma public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) return; - - if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) return; - - if (Math.Sign(newStun) != Math.Sign(Stun)) AnimController.ResetPullJoints(); - + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } + if (Screen.Selected != GameMain.GameScreen) { return; } + if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; } + if (Math.Sign(newStun) != Math.Sign(Stun)) + { + AnimController.ResetPullJoints(); + } CharacterHealth.StunTimer = newStun; if (newStun > 0.0f) { @@ -2514,7 +2608,7 @@ namespace Barotrauma if (!isNetworkMessage) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; } Kill(CauseOfDeathType.Pressure, null, isNetworkMessage); @@ -2559,7 +2653,7 @@ namespace Barotrauma if (IsDead || CharacterHealth.Unkillable) { return; } HealthUpdateInterval = 0.0f; - + //clients aren't allowed to kill characters unless they receive a network message if (!isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { @@ -2569,7 +2663,7 @@ namespace Barotrauma ApplyStatusEffects(ActionType.OnDeath, 1.0f); AnimController.Frozen = false; - + if (GameSettings.SendUserStatistics) { string characterType = "Unknown"; @@ -2589,7 +2683,7 @@ namespace Barotrauma } CauseOfDeath = new CauseOfDeath( - causeOfDeath, causeOfDeathAffliction?.Prefab, + causeOfDeath, causeOfDeathAffliction?.Prefab, causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); OnDeath?.Invoke(this, CauseOfDeath); @@ -2603,10 +2697,11 @@ namespace Barotrauma AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; - for (int i = 0; i < selectedItems.Length; i++ ) + for (int i = 0; i < selectedItems.Length; i++) { - if (selectedItems[i] != null) selectedItems[i].Drop(this); + if (selectedItems[i] != null) selectedItems[i].Drop(this); } + SelectedConstruction = null; AnimController.ResetPullJoints(); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs index 6ddafd729..536efd748 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs @@ -86,8 +86,6 @@ namespace Barotrauma } } - private static Dictionary cachedConfigs = new Dictionary(); - private static ushort idCounter; public string Name; @@ -128,14 +126,24 @@ namespace Barotrauma } } - public string SpeciesName => SourceElement.GetAttributeString("name", string.Empty); + private string _speciesName; + public string SpeciesName + { + get + { + if (_speciesName == null) + { + _speciesName = CharacterConfigElement.GetAttributeString("speciesname", string.Empty).ToLowerInvariant(); + } + return _speciesName; + } + set { _speciesName = value; } + } /// /// Note: Can be null. /// public Character Character; - - public readonly string File; public Job Job; @@ -190,7 +198,7 @@ namespace Barotrauma { if (portraitBackground == null) { - var portraitBackgroundElement = SourceElement.Element("portraitbackground"); + var portraitBackgroundElement = CharacterConfigElement.Element("portraitbackground"); if (portraitBackgroundElement != null) { portraitBackground = new Sprite(portraitBackgroundElement.Element("sprite")); @@ -229,7 +237,7 @@ namespace Barotrauma } } - public XElement SourceElement { get; set; } + public XElement CharacterConfigElement { get; set; } public readonly string ragdollFileName = string.Empty; @@ -329,7 +337,7 @@ namespace Barotrauma if (ragdoll == null) { string speciesName = SpeciesName; - bool isHumanoid = SourceElement.GetAttributeBool("humanoid", false); + bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(Character.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)); ragdoll = isHumanoid ? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName) : RagdollParams.GetRagdollParams(speciesName, ragdollFileName) as RagdollParams; @@ -342,16 +350,21 @@ namespace Barotrauma public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; // Used for creating the data - public CharacterInfo(string file, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null) + public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null) { + if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + { + speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); + } ID = idCounter; idCounter++; - File = file; + _speciesName = speciesName; SpriteTags = new List(); - XDocument doc = GetConfig(file); - SourceElement = doc.Root; + XDocument doc = Character.GetConfigFile(_speciesName); + if (doc == null) { return; } + CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; head = new HeadInfo(); - HasGenders = doc.Root.GetAttributeBool("genders", false); + HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); if (HasGenders) { Head.gender = GetRandomGender(); @@ -367,16 +380,16 @@ namespace Barotrauma else { name = ""; - if (doc.Root.Element("name") != null) + if (CharacterConfigElement.Element("name") != null) { - string firstNamePath = doc.Root.Element("name").GetAttributeString("firstname", ""); + string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); if (firstNamePath != "") { firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); Name = ToolBox.GetRandomLine(firstNamePath); } - string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", ""); + string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); if (lastNamePath != "") { lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); @@ -395,18 +408,30 @@ namespace Barotrauma } // Used for loading the data - public CharacterInfo(XElement element) + public CharacterInfo(XElement infoElement) { ID = idCounter; idCounter++; - Name = element.GetAttributeString("name", ""); - string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant(); - File = element.GetAttributeString("file", ""); - SourceElement = GetConfig(File).Root; - HasGenders = SourceElement.GetAttributeBool("genders", false); - Salary = element.GetAttributeInt("salary", 1000); - Enum.TryParse(element.GetAttributeString("race", "White"), true, out Race race); - Enum.TryParse(element.GetAttributeString("gender", "None"), true, out Gender gender); + Name = infoElement.GetAttributeString("name", ""); + string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant(); + Salary = infoElement.GetAttributeInt("salary", 1000); + Enum.TryParse(infoElement.GetAttributeString("race", "White"), 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) + { + doc = Character.GetConfigFile(_speciesName); + } + else + { + // Backwards support (human only) + string file = infoElement.GetAttributeString("file", ""); + doc = XMLExtensions.TryLoadXml(file); + } + if (doc == null) { return; } + CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); if (HasGenders && gender == Gender.None) { gender = GetRandomGender(); @@ -416,26 +441,26 @@ namespace Barotrauma gender = Gender.None; } RecreateHead( - element.GetAttributeInt("headspriteid", 1), + infoElement.GetAttributeInt("headspriteid", 1), race, gender, - element.GetAttributeInt("hairindex", -1), - element.GetAttributeInt("beardindex", -1), - element.GetAttributeInt("moustacheindex", -1), - element.GetAttributeInt("faceattachmentindex", -1)); + infoElement.GetAttributeInt("hairindex", -1), + infoElement.GetAttributeInt("beardindex", -1), + infoElement.GetAttributeInt("moustacheindex", -1), + infoElement.GetAttributeInt("faceattachmentindex", -1)); if (string.IsNullOrEmpty(Name)) { - if (SourceElement.Element("name") != null) + if (CharacterConfigElement.Element("name") != null) { - string firstNamePath = SourceElement.Element("name").GetAttributeString("firstname", ""); + string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); if (firstNamePath != "") { firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); Name = ToolBox.GetRandomLine(firstNamePath); } - string lastNamePath = SourceElement.Element("name").GetAttributeString("lastname", ""); + string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); if (lastNamePath != "") { lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); @@ -445,15 +470,14 @@ namespace Barotrauma } } - - StartItemsGiven = element.GetAttributeBool("startitemsgiven", false); - string personalityName = element.GetAttributeString("personality", ""); - ragdollFileName = element.GetAttributeString("ragdoll", string.Empty); + StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false); + string personalityName = infoElement.GetAttributeString("personality", ""); + ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty); if (!string.IsNullOrEmpty(personalityName)) { personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName); } - foreach (XElement subElement in element.Elements()) + foreach (XElement subElement in infoElement.Elements()) { if (subElement.Name.ToString().ToLowerInvariant() != "job") continue; Job = new Job(subElement); @@ -462,20 +486,9 @@ namespace Barotrauma LoadHeadAttachments(); } - private XDocument GetConfig(string file) - { - if (!cachedConfigs.TryGetValue(file, out XDocument doc)) - { - doc = XMLExtensions.TryLoadXml(file); - if (doc == null) { return null; } - cachedConfigs.Add(file, doc); - } - return doc; - } - public int SetRandomHead() => HeadSpriteId = GetRandomHeadID(); - public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < SourceElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; + public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; public Race GetRandomRace() => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server); public int GetRandomHeadID() => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, Rand.RandSync.Server) : 0; @@ -491,7 +504,7 @@ namespace Barotrauma { if (wearables == null) { - var attachments = SourceElement.Element("HeadAttachments"); + var attachments = CharacterConfigElement.Element("HeadAttachments"); if (attachments != null) { wearables = attachments.Elements("Wearable"); @@ -503,6 +516,7 @@ namespace Barotrauma public IEnumerable FilterByTypeAndHeadID(IEnumerable elements, WearableType targetType) { + if (elements == null) { return elements; } return elements.Where(e => { if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; } @@ -522,8 +536,8 @@ namespace Barotrauma private void CalculateHeadSpriteRange() { - if (SourceElement == null) { return; } - Head.headSpriteRange = SourceElement.GetAttributeVector2("headidrange", Vector2.Zero); + if (CharacterConfigElement == null) { return; } + Head.headSpriteRange = CharacterConfigElement.GetAttributeVector2("headidrange", Vector2.Zero); // If range is defined, we use it as it is // Else we calculate the range from the wearables. if (Head.headSpriteRange == Vector2.Zero) @@ -582,11 +596,13 @@ namespace Barotrauma public void LoadHeadSprite() { + // TODO: use ragdollparams instead? foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { - if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") continue; + if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; } XElement spriteElement = limbElement.Element("sprite"); + if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; @@ -605,7 +621,7 @@ namespace Barotrauma } string fileWithoutTags = Path.GetFileNameWithoutExtension(file); fileWithoutTags = fileWithoutTags.Split('[', ']').First(); - if (fileWithoutTags != fileName) continue; + if (fileWithoutTags != fileName) { continue; } HeadSprite = new Sprite(spriteElement, "", file); Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; @@ -788,7 +804,7 @@ namespace Barotrauma charElement.Add( new XAttribute("name", Name), - new XAttribute("file", File), + new XAttribute("speciesname", SpeciesName), new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"), new XAttribute("race", Head.race.ToString()), new XAttribute("salary", Salary), diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs index fc826ad7f..574f2c273 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs @@ -2,14 +2,26 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { - class Affliction + class Affliction : ISerializableEntity { public readonly AfflictionPrefab Prefab; - - public float Strength; + + public string Name => ToString(); + + public Dictionary SerializableProperties { get; set; } + + [Serialize(0f, true), Editable] + public float Strength { get; set; } + + [Serialize("", true), Editable] + public string Identifier { get; private set; } + + [Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)] + public float Probability { get; private set; } = 1.0f; public float DamagePerSecond; public float DamagePerSecondTimer; @@ -18,11 +30,6 @@ namespace Barotrauma public float StrengthDiminishMultiplier = 1.0f; public Affliction MultiplierSource; - /// - /// Probability for the affliction to be applied. Used by attacks. - /// - public float ApplyProbability = 1.0f; - /// /// Which character gave this affliction /// @@ -32,6 +39,17 @@ namespace Barotrauma { Prefab = prefab; Strength = strength; + Identifier = prefab?.Identifier; + } + + public void Serialize(XElement element) + { + SerializableProperty.SerializeProperties(this, element); + } + + public void Deserialize(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } public Affliction CreateMultiplied(float multiplier) @@ -39,10 +57,7 @@ namespace Barotrauma return Prefab.Instantiate(Strength * multiplier, Source); } - public override string ToString() - { - return "Affliction (" + Prefab.Name + ")"; - } + public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})"; public float GetVitalityDecrease(CharacterHealth characterHealth) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionHusk.cs index 234203834..f62c1181b 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionHusk.cs @@ -22,7 +22,7 @@ namespace Barotrauma { get { return state; } } - + public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { @@ -96,60 +96,27 @@ namespace Barotrauma } } - private void ActivateHusk(Character character) + public void ActivateHusk(Character character) { - character.NeedsAir = false; if (huskAppendage == null) { - huskAppendage = AttachHuskAppendage(character); - character.SetStun(0.5f); - } - } - - public static List AttachHuskAppendage(Character character, Ragdoll ragdoll = null) - { - var huskDoc = XMLExtensions.TryLoadXml(Character.GetConfigFile("humanhusk")); - string pathToAppendage = huskDoc.Root.Element("huskappendage").GetAttributeString("path", string.Empty); - XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage); - if (doc == null || doc.Root == null) { return null; } - if (ragdoll == null) - { - ragdoll = character.AnimController; - } - if (ragdoll.Dir < 1.0f) - { - ragdoll.Flip(); - } - var huskAppendages = new List(); - var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); - foreach (var jointElement in doc.Root.Elements("joint")) - { - if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement)) + huskAppendage = AttachHuskAppendage(character, Prefab.Identifier); + if (huskAppendage != null) { - JointParams jointParams = new JointParams(jointElement, ragdoll.RagdollParams); - Limb attachLimb = ragdoll.Limbs[jointParams.Limb1]; - Limb huskAppendage = new Limb(ragdoll, character, new LimbParams(limbElement, ragdoll.RagdollParams)); - huskAppendage.body.Submarine = character.Submarine; - huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation); - ragdoll.AddLimb(huskAppendage); - ragdoll.AddJoint(jointParams); - huskAppendages.Add(huskAppendage); + character.NeedsAir = false; + character.SetStun(0.5f); } } - return huskAppendages; } private void DeactivateHusk(Character character) { - character.NeedsAir = true; - RemoveHuskAppendage(character); - } - - private void RemoveHuskAppendage(Character character) - { - if (huskAppendage == null) return; - huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l)); - huskAppendage = null; + character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); + if (huskAppendage != null) + { + huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l)); + huskAppendage = null; + } } public void Remove(Character character) @@ -182,7 +149,8 @@ namespace Barotrauma character.Enabled = false; Entity.Spawner.AddToRemoveQueue(character); - var configFile = Character.GetConfigFile("humanhusk"); + string speciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); + string configFile = Character.GetConfigFilePath(speciesName); if (string.IsNullOrEmpty(configFile)) { @@ -190,16 +158,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - //XDocument doc = XMLExtensions.TryLoadXml(configFile); - //if (doc?.Root == null) - //{ - // DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file ("+configFile+") could not be read."); - // yield return CoroutineStatus.Success; - //} - - //character.Info.Ragdoll = null; - //character.Info.SourceElement = doc.Root; - var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true); + var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true, ragdoll: character.AnimController.RagdollParams); foreach (Limb limb in husk.AnimController.Limbs) { @@ -220,7 +179,7 @@ namespace Barotrauma if (character.Inventory.Items.Length != husk.Inventory.Items.Length) { - string errorMsg = "Failed to move items from a human's inventory into a humanhusk's inventory (inventory sizes don't match)"; + string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); yield return CoroutineStatus.Success; @@ -234,5 +193,103 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + + public static List AttachHuskAppendage(Character character, string afflictionIdentifier, XElement appendageDefinition = null, Ragdoll ragdoll = null) + { + var appendage = new List(); + if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction)) + { + 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); + string filePath = Character.GetConfigFilePath(huskedSpeciesName); + if (!Character.TryGetConfigFile(filePath, out XDocument huskDoc)) + { + DebugConsole.ThrowError($"Error in '{filePath}': Failed to load the config file for the husk infected species with the species name '{huskedSpeciesName}'!"); + return appendage; + } + var mainElement = huskDoc.Root.IsOverride() ? huskDoc.Root.FirstElement() : huskDoc.Root; + var element = appendageDefinition; + if (element == null) + { + element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier)); + } + if (element == null) + { + DebugConsole.ThrowError($"Error in '{filePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!"); + return appendage; + } + string pathToAppendage = element.GetAttributeString("path", string.Empty); + XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage); + if (doc == null) { return appendage; } + if (ragdoll == null) + { + ragdoll = character.AnimController; + } + if (ragdoll.Dir < 1.0f) + { + ragdoll.Flip(); + } + var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); + foreach (var jointElement in doc.Root.Elements("joint")) + { + if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement)) + { + var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams); + Limb attachLimb = null; + if (matchingAffliction.AttachLimbId > -1) + { + attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == matchingAffliction.AttachLimbId); + } + else if (matchingAffliction.AttachLimbName != null) + { + attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Name == matchingAffliction.AttachLimbName); + } + else if (matchingAffliction.AttachLimbType != LimbType.None) + { + attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.type == matchingAffliction.AttachLimbType); + } + if (attachLimb == null) + { + DebugConsole.Log("Attachment limb not defined in the affliction prefab or no matching limb could be found. Using the appendage definition as it is."); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == jointParams.Limb1); + } + if (attachLimb != null) + { + jointParams.Limb1 = attachLimb.Params.ID; + var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams) + { + // Ensure that we have a valid id for the new limb + ID = ragdoll.Limbs.Length + }; + jointParams.Limb2 = appendageLimbParams.ID; + Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams); + huskAppendage.body.Submarine = character.Submarine; + huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation); + ragdoll.AddLimb(huskAppendage); + ragdoll.AddJoint(jointParams); + appendage.Add(huskAppendage); + } + else + { + DebugConsole.ThrowError("Attachment limb not found!"); + } + } + } + return appendage; + } + + public static string GetHuskedSpeciesName(string speciesName, AfflictionPrefabHusk prefab) + { + return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName); + } + + public static string GetNonHuskedSpeciesName(string huskedSpeciesName, AfflictionPrefabHusk prefab) + { + string nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag); + return huskedSpeciesName.Remove(nonTag); + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs index c6d60ea31..f1a1a8ee1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Xml.Linq; +using System.Linq; namespace Barotrauma { public static class CPRSettings { + 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; } @@ -31,9 +33,52 @@ namespace Barotrauma DamageSkillThreshold = MathHelper.Clamp(element.GetAttributeFloat("damageskillthreshold", 40.0f), 0.0f, 100.0f); DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f); + IsLoaded = true; } } + class AfflictionPrefabHusk : AfflictionPrefab + { + public AfflictionPrefabHusk(XElement element, Type type = null) : base(element, type) + { + HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null); + if (HuskedSpeciesName == null) + { + DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange); + HuskedSpeciesName = "[speciesname]husk"; + } + TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true); + if (TargetSpecies.Length == 0) + { + DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange); + TargetSpecies = new string[] { "human" }; + } + var attachElement = element.GetChildElement("attachlimb"); + if (attachElement != null) + { + AttachLimbId = attachElement.GetAttributeInt("id", -1); + AttachLimbName = attachElement.GetAttributeString("name", null); + AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None; + } + else + { + AttachLimbId = -1; + AttachLimbName = null; + AttachLimbType = LimbType.None; + } + } + + // Use any of these to define which limb the appendage is attached to. + // If multiple are defined, the order of preference is: id, name, type. + public readonly int AttachLimbId; + public readonly string AttachLimbName; + public readonly LimbType AttachLimbType; + + public readonly string HuskedSpeciesName; + public readonly string[] TargetSpecies; + public const string Tag = "[speciesname]"; + } + class AfflictionPrefab { public class Effect @@ -126,7 +171,6 @@ namespace Barotrauma public static AfflictionPrefab Bloodloss; public static AfflictionPrefab Pressure; public static AfflictionPrefab Stun; - public static AfflictionPrefab Husk; public static List List = new List(); @@ -187,44 +231,109 @@ namespace Barotrauma foreach (string filePath in filePaths) { XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) + if (doc == null) { continue; } + var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + if (doc.Root.IsOverride()) { - switch (element.Name.ToString().ToLowerInvariant()) + DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one."); + } + 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)) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + DebugConsole.ThrowError($"No identifier defined for the affliction '{elementName}' in file '{filePath}'"); + continue; + } + var duplicate = List.FirstOrDefault(a => a.Identifier == identifier); + if (duplicate != null) + { + if (isOverride) + { + DebugConsole.NewMessage($"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{filePath}'", Color.Yellow); + List.Remove(duplicate); + } + else + { + DebugConsole.ThrowError($"Duplicate affliction: '{identifier}' defined in {elementName} of '{filePath}'"); + continue; + } + } + } + string type = sourceElement.GetAttributeString("type", null); + if (sourceElement.Name.ToString().ToLowerInvariant() == "cprsettings") + { + //backwards compatibility + type = "cprsettings"; + } + + AfflictionPrefab prefab = null; + switch (type) { - case "internaldamage": - List.Add(InternalDamage = new AfflictionPrefab(element, typeof(Affliction))); - break; case "bleeding": - List.Add(Bleeding = new AfflictionPrefab(element, typeof(AfflictionBleeding))); + prefab = new AfflictionPrefab(sourceElement, typeof(AfflictionBleeding)); break; - case "burn": - List.Add(Burn = new AfflictionPrefab(element, typeof(Affliction))); - break; - case "oxygenlow": - List.Add(OxygenLow = new AfflictionPrefab(element, typeof(Affliction))); - break; - case "bloodloss": - List.Add(Bloodloss = new AfflictionPrefab(element, typeof(Affliction))); - break; - case "pressure": - List.Add(Pressure = new AfflictionPrefab(element, typeof(Affliction))); - break; - case "stun": - List.Add(Stun = new AfflictionPrefab(element, typeof(Affliction))); - break; - case "husk": - case "afflictionhusk": - List.Add(Husk = new AfflictionPrefab(element, typeof(AfflictionHusk))); + case "huskinfection": + prefab = new AfflictionPrefabHusk(sourceElement, typeof(AfflictionHusk)); break; case "cprsettings": - CPRSettings.Load(element); + if (CPRSettings.IsLoaded) + { + if (isOverride) + { + DebugConsole.NewMessage($"Overriding the CPR settings with '{filePath}'", Color.Yellow); + } + else + { + DebugConsole.ThrowError($"Error in '{filePath}': CPR settings already loaded. Add tags as the parent of the custom CPRSettings to allow overriding the vanilla values."); + break; + } + } + CPRSettings.Load(sourceElement); + break; + case "damage": + case "burn": + case "oxygenlow": + case "bloodloss": + case "stun": + case "pressure": + case "internaldamage": + prefab = new AfflictionPrefab(sourceElement, typeof(Affliction)); break; default: - List.Add(new AfflictionPrefab(element)); + prefab = new AfflictionPrefab(sourceElement); break; } + switch (identifier) + { + case "internaldamage": + InternalDamage = 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; + } + if (prefab != null) { List.Add(prefab); } } } @@ -235,12 +344,15 @@ namespace Barotrauma 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 (Husk == null) DebugConsole.ThrowError("Affliction \"Husk\" not defined in the affliction prefabs."); } public AfflictionPrefab(XElement element, Type type = null) { typeName = type == null ? element.Name.ToString() : type.Name; + if (typeName == "InternalDamage" && type == null) + { + type = typeof(Affliction); + } Identifier = element.GetAttributeString("identifier", ""); @@ -305,7 +417,7 @@ namespace Barotrauma catch { DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\"."); - return; + type = typeof(Affliction); } constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs index fb71dbcd9..ee7a53b8b 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; +using Barotrauma.Extensions; namespace Barotrauma { @@ -78,14 +79,33 @@ namespace Barotrauma public const float InsufficientOxygenThreshold = 30.0f; public const float LowOxygenThreshold = 50.0f; - protected float minVitality, maxVitality; + protected float minVitality; + + protected float maxVitality + { + get => Character.Params.Health.Vitality; + set => Character.Params.Health.Vitality = value; + } public bool Unkillable; - //bleeding settings - public bool DoesBleed { get; private set; } + public bool DoesBleed + { + get => Character.Params.Health.DoesBleed; + private set => Character.Params.Health.DoesBleed = value; + } - public bool UseHealthWindow { get; set; } + public bool UseHealthWindow + { + get => Character.Params.Health.UseHealthWindow; + set => Character.Params.Health.UseHealthWindow = value; + } + + public float CrushDepth + { + get => Character.Params.Health.CrushDepth; + private set => Character.Params.Health.CrushDepth = value; + } private List limbHealths = new List(); //non-limb-specific afflictions @@ -102,11 +122,12 @@ namespace Barotrauma get { return Vitality <= 0.0f; } } - public float CrushDepth { get; private set; } public float PressureKillDelay { get; private set; } = 5.0f; public float Vitality { get; private set; } + public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality); + public float MaxVitality { get @@ -168,7 +189,6 @@ namespace Barotrauma { this.Character = character; Vitality = 100.0f; - maxVitality = 100.0f; DoesBleed = true; UseHealthWindow = false; @@ -185,15 +205,9 @@ namespace Barotrauma this.Character = character; InitIrremovableAfflictions(); - CrushDepth = element.GetAttributeFloat("crushdepth", float.NegativeInfinity); - - maxVitality = element.GetAttributeFloat("vitality", 100.0f); Vitality = maxVitality; - DoesBleed = element.GetAttributeBool("doesbleed", true); - UseHealthWindow = element.GetAttributeBool("usehealthwindow", false); - - minVitality = (character.ConfigPath == Character.HumanConfigFile) ? -100.0f : 0.0f; + minVitality = character.IsHuman ? -100.0f : 0.0f; limbHealths.Clear(); foreach (XElement subElement in element.Elements()) @@ -225,10 +239,9 @@ namespace Barotrauma public IEnumerable GetAllAfflictions(Func limbHealthFilter = null) { - // TODO: If there can be duplicates, we should use Union instead. return limbHealthFilter == null - ? afflictions.Concat(limbHealths.SelectMany(lh => lh.Afflictions)) - : afflictions.Concat(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter))); + ? afflictions.Union(limbHealths.SelectMany(lh => lh.Afflictions)) + : afflictions.Where(limbHealthFilter).Union(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter))); } private LimbHealth GetMatchingLimbHealth(Limb limb) => limbHealths[limb.HealthIndex]; @@ -240,11 +253,23 @@ namespace Barotrauma private IEnumerable GetMatchingAfflictions(LimbHealth limb, Func predicate) => limb.Afflictions.Where(predicate).Union(afflictions.Where(a => predicate(a) && GetMatchingLimbHealth(a) == limb)); - public Affliction GetAffliction(string afflictionType, bool allowLimbAfflictions = true) + public IEnumerable GetAfflictionsByType(string afflictionType, bool allowLimbAfflictions = true) + { + if (allowLimbAfflictions) + { + return GetAllAfflictions(a => a.Prefab.AfflictionType == afflictionType); + } + else + { + return afflictions.Where(a => a.Prefab.AfflictionType == afflictionType); + } + } + + public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) { foreach (Affliction affliction in afflictions) { - if (affliction.Prefab.AfflictionType == afflictionType) return affliction; + if (affliction.Prefab.Identifier == identifier) return affliction; } if (!allowLimbAfflictions) return null; @@ -252,19 +277,30 @@ namespace Barotrauma { foreach (Affliction affliction in limbHealth.Afflictions) { - if (affliction.Prefab.AfflictionType == afflictionType) return affliction; + if (affliction.Prefab.Identifier == identifier) return affliction; } } return null; } - public T GetAffliction(string afflictionType, bool allowLimbAfflictions = true) where T : Affliction + public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction { - return GetAffliction(afflictionType, allowLimbAfflictions) as T; + return GetAffliction(identifier, allowLimbAfflictions) as T; } - public Affliction GetAffliction(string afflictionType, Limb limb) + public IEnumerable GetAfflictionsByType(string afflictionType, Limb limb) + { + if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) + { + DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name + + "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex); + return null; + } + return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType); + } + + public Affliction GetAffliction(string identifier, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { @@ -274,7 +310,7 @@ namespace Barotrauma } foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions) { - if (affliction.Prefab.AfflictionType == afflictionType) return affliction; + if (affliction.Prefab.Identifier == identifier) return affliction; } return null; } @@ -520,8 +556,14 @@ namespace Barotrauma { if (!DoesBleed && newAffliction is AfflictionBleeding) return; if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; - // Currently only human can get the husk infection. - if (newAffliction.Prefab == AfflictionPrefab.Husk && Character.SpeciesName.ToLowerInvariant() != "human") { return; } + if (newAffliction.Prefab.AfflictionType == "huskinfection") + { + var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk; + if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase))) + { + return; + } + } foreach (Affliction affliction in afflictions) { if (newAffliction.Prefab == affliction.Prefab) @@ -547,7 +589,10 @@ namespace Barotrauma CalculateVitality(); if (Vitality <= MinVitality) Kill(); } - + + + partial void UpdateProjSpecific(float deltaTime); + partial void UpdateLimbAfflictionOverlays(); public void Update(float deltaTime) @@ -671,6 +716,10 @@ namespace Barotrauma var causeOfDeath = GetCauseOfDeath(); Character.Kill(causeOfDeath.First, causeOfDeath.Second); +#if CLIENT + DisplayVitalityDelay = 0.0f; + DisplayedVitality = Vitality; +#endif } public Pair GetCauseOfDeath() diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/DamageModifier.cs index c10c55bdb..8eb77c3ac 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/DamageModifier.cs @@ -1,86 +1,120 @@ using Microsoft.Xna.Framework; +using System; using System.Xml.Linq; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma { - partial class DamageModifier + partial class DamageModifier : ISerializableEntity { - [Serialize(1.0f, false)] + public string Name => "Damage Modifier"; + + public Dictionary SerializableProperties { get; private set; } + + [Serialize(1.0f, false), Editable(DecimalCount = 2)] public float DamageMultiplier { get; private set; } - [Serialize("0.0,360", false)] + [Serialize("0.0,360", false), Editable] public Vector2 ArmorSector { get; private set; } - [Serialize(true, false)] - public bool IsArmor - { - get; - private set; - } + public Vector2 ArmorSectorInRadians => new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y)); - [Serialize(false, false)] + [Serialize(false, false), Editable] public bool DeflectProjectiles { get; private set; } - public string[] AfflictionIdentifiers + [Serialize("", true), Editable] + public string AfflictionIdentifiers { - get; - private set; + get + { + return rawAfflictionIdentifierString; + } + private set + { + rawAfflictionIdentifierString = value; + ParseAfflictionIdentifiers(); + } } - public string[] AfflictionTypes + [Serialize("", true), Editable] + public string AfflictionTypes { - get; - private set; + get + { + return rawAfflictionTypeString; + } + private set + { + rawAfflictionTypeString = value; + ParseAfflictionTypes(); + } } + private string rawAfflictionIdentifierString; + private string rawAfflictionTypeString; + private string[] parsedAfflictionIdentifiers; + private string[] parsedAfflictionTypes; + public DamageModifier(XElement element, string parentDebugName) { - SerializableProperty.DeserializeProperties(this, element); - ArmorSector = new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y)); - + Deserialize(element); if (element.Attribute("afflictionnames") != null) { DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); } + } - AfflictionIdentifiers = element.GetAttributeStringArray("afflictionidentifiers", new string[0]); - for (int i = 0; i < AfflictionIdentifiers.Length; i++) + private void ParseAfflictionTypes() + { + string[] splitValue = rawAfflictionTypeString.Split(',', ','); + for (int i = 0; i < splitValue.Length; i++) { - AfflictionIdentifiers[i] = AfflictionIdentifiers[i].ToLowerInvariant(); + splitValue[i] = splitValue[i].ToLowerInvariant().Trim(); } - AfflictionTypes = element.GetAttributeStringArray("afflictiontypes", new string[0]); - for (int i = 0; i < AfflictionTypes.Length; i++) + parsedAfflictionTypes = splitValue; + } + + private void ParseAfflictionIdentifiers() + { + string[] splitValue = rawAfflictionIdentifierString.Split(',', ','); + for (int i = 0; i < splitValue.Length; i++) { - AfflictionTypes[i] = AfflictionTypes[i].ToLowerInvariant(); + splitValue[i] = splitValue[i].ToLowerInvariant().Trim(); } + parsedAfflictionIdentifiers = splitValue; } public bool MatchesAffliction(Affliction affliction) { //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(affliction.Identifier, StringComparison.OrdinalIgnoreCase)) + || parsedAfflictionTypes.Any(t => t.Equals(affliction.Prefab.AfflictionType, StringComparison.OrdinalIgnoreCase)); + } - foreach (string afflictionName in AfflictionIdentifiers) - { - if (affliction.Prefab.Identifier.ToLowerInvariant() == afflictionName) return true; - } - foreach (string afflictionType in AfflictionTypes) - { - if (affliction.Prefab.AfflictionType.ToLowerInvariant() == afflictionType) return true; - } - return false; + public void Serialize(XElement element) + { + if (element == null) { return; } + SerializableProperty.SerializeProperties(this, element); + } + + public void Deserialize(XElement element) + { + if (element == null) { return; } + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs index deea9e2ab..979f97eb7 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs @@ -50,38 +50,25 @@ namespace Barotrauma public Job(XElement element) { string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant(); - prefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == identifier); - - string name = ""; - if (prefab == null) + if (!JobPrefab.List.TryGetValue(identifier, out JobPrefab p)) { - name = element.GetAttributeString("name", "").ToLowerInvariant(); - prefab = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == name); + DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job."); + p = JobPrefab.Random(); } - if (prefab == null) - { - DebugConsole.ThrowError("Could not find the job \"" + name + "\" (identifier " + identifier + "). Giving the character a random job."); - prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count)]; - } - + prefab = p; skills = new Dictionary(); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "skill") continue; + if (subElement.Name.ToString().ToLowerInvariant() != "skill") { continue; } string skillIdentifier = subElement.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(skillIdentifier)) continue; + if (string.IsNullOrEmpty(skillIdentifier)) { continue; } skills.Add( skillIdentifier, new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0))); } } - public static Job Random(Rand.RandSync randSync) - { - JobPrefab prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count - 1, randSync)]; - - return new Job(prefab); - } + public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced) => new Job(JobPrefab.Random(randSync)); public float GetSkillLevel(string skillIdentifier) { @@ -186,7 +173,7 @@ namespace Barotrauma wifiComponent.TeamID = character.TeamID; } - if (parentItem != null) parentItem.Combine(item); + if (parentItem != null) parentItem.Combine(item, user: null); foreach (XElement childItemElement in itemElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs index 7261ab890..cc09ccc9f 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs @@ -2,18 +2,26 @@ using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Linq; namespace Barotrauma { public class AutonomousObjective { - public string aiTag; + public string identifier; public string option; public float priorityModifier; public AutonomousObjective(XElement element) { - aiTag = element.GetAttributeString("aitag", null); + identifier = element.GetAttributeString("identifier", null); + + //backwards compatibility + if (string.IsNullOrEmpty(identifier)) + { + identifier = element.GetAttributeString("aitag", null); + } + option = element.GetAttributeString("option", null); priorityModifier = element.GetAttributeFloat("prioritymodifier", 1); priorityModifier = MathHelper.Max(priorityModifier, 0); @@ -22,13 +30,32 @@ namespace Barotrauma partial class JobPrefab { - public static List List; + public static Dictionary List; + public static JobPrefab Get(string identifier) + { + if (List == null) + { + DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded."); + return null; + } + if (List.TryGetValue(identifier, out JobPrefab job)) + { + return job; + } + else + { + DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier); + return null; + } + } public readonly XElement Items; public readonly List ItemNames = new List(); public readonly List Skills = new List(); public readonly List AutomaticOrders = new List(); - + public readonly List AppropriateOrders = new List(); + + [Serialize("1,1,1,1", false)] public Color UIColor { @@ -126,6 +153,7 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, element); Name = TextManager.Get("JobName." + Identifier); Description = TextManager.Get("JobDescription." + Identifier); + Identifier = Identifier.ToLowerInvariant(); foreach (XElement subElement in element.Elements()) { @@ -133,35 +161,7 @@ namespace Barotrauma { case "items": Items = subElement; - foreach (XElement itemElement in subElement.Elements()) - { - if (itemElement.Element("name") != null) - { - DebugConsole.ThrowError("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items."); - ItemNames.Add(itemElement.GetAttributeString("name", "")); - continue; - } - - string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - if (string.IsNullOrWhiteSpace(itemIdentifier)) - { - DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier."); - ItemNames.Add(""); - } - else - { - var prefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; - if (prefab == null) - { - DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item prefab \""+itemIdentifier+"\" not found."); - ItemNames.Add(""); - } - else - { - ItemNames.Add(prefab.Name); - } - } - } + loadItemNames(subElement); break; case "skills": foreach (XElement skillElement in subElement.Elements()) @@ -172,6 +172,44 @@ namespace Barotrauma case "autonomousobjectives": subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order))); break; + case "appropriateobjectives": + case "appropriateorders": + subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeString("identifier", "").ToLowerInvariant())); + break; + } + } + + void loadItemNames(XElement parentElement) + { + foreach (XElement itemElement in parentElement.Elements()) + { + if (itemElement.Element("name") != null) + { + DebugConsole.ThrowError("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items."); + ItemNames.Add(itemElement.GetAttributeString("name", "")); + continue; + } + + string itemIdentifier = itemElement.GetAttributeString("identifier", ""); + if (string.IsNullOrWhiteSpace(itemIdentifier)) + { + DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier."); + ItemNames.Add(""); + } + else + { + var prefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + if (prefab == null) + { + DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item prefab \"" + itemIdentifier + "\" not found."); + ItemNames.Add(""); + } + else + { + ItemNames.Add(prefab.Name); + } + } + loadItemNames(itemElement); } } @@ -184,24 +222,45 @@ namespace Barotrauma } } - public static JobPrefab Random() - { - return List[Rand.Int(List.Count)]; - } + public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => List.Values.GetRandom(sync); public static void LoadAll(IEnumerable filePaths) { - List = new List(); + List = new Dictionary(); foreach (string filePath in filePaths) { XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) return; - - foreach (XElement element in doc.Root.Elements()) + if (doc == null) { continue; } + var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + if (doc.Root.IsOverride()) { - JobPrefab job = new JobPrefab(element); - List.Add(job); + DebugConsole.ThrowError($"Error in '{filePath}': 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()); + if (List.TryGetValue(job.Identifier, out JobPrefab duplicate)) + { + DebugConsole.NewMessage($"Overriding the job '{duplicate.Identifier}' with another defined in '{filePath}'", Color.Yellow); + List.Remove(duplicate.Identifier); + } + List.Add(job.Identifier, job); + } + else + { + if (List.TryGetValue(element.GetAttributeString("identifier", "").ToLowerInvariant(), out JobPrefab duplicate)) + { + DebugConsole.ThrowError($"Error in '{filePath}': Duplicate job definition found for: '{duplicate.Identifier}'. Use the XML element as the parent of job element's definition to override the existing job."); + } + else + { + var job = new JobPrefab(element); + List.Add(job.Identifier, job); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs index e2f9b50bc..c394e5e6a 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs @@ -9,6 +9,8 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; +using LimbParams = Barotrauma.RagdollParams.LimbParams; +using JointParams = Barotrauma.RagdollParams.JointParams; namespace Barotrauma { @@ -21,14 +23,14 @@ namespace Barotrauma partial class LimbJoint : RevoluteJoint { public bool IsSevered; - public bool CanBeSevered => jointParams.CanBeSevered; - public readonly JointParams jointParams; + public bool CanBeSevered => Params.CanBeSevered; + public readonly JointParams Params; public readonly Ragdoll ragdoll; public readonly Limb LimbA, LimbB; public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One) { - this.jointParams = jointParams; + Params = jointParams; this.ragdoll = ragdoll; LoadParams(); } @@ -43,63 +45,37 @@ namespace Barotrauma LimbB = limbB; } - public void SaveParams() - { - // Saving to the params is handled only in the params level. - return; - - jointParams.Stiffness = MaxMotorTorque; - if (ragdoll.IsFlipped) - { - jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorA.X, LocalAnchorA.Y) / jointParams.Ragdoll.JointScale); - jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorB.X, LocalAnchorB.Y) / jointParams.Ragdoll.JointScale); - jointParams.UpperLimit = MathHelper.ToDegrees(-LowerLimit); - jointParams.LowerLimit = MathHelper.ToDegrees(-UpperLimit); - } - else - { - jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorA / jointParams.Ragdoll.JointScale); - jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorB / jointParams.Ragdoll.JointScale); - jointParams.UpperLimit = MathHelper.ToDegrees(UpperLimit); - jointParams.LowerLimit = MathHelper.ToDegrees(LowerLimit); - } - } - public void LoadParams() { - MaxMotorTorque = jointParams.Stiffness; - LimitEnabled = jointParams.LimitEnabled; - if (float.IsNaN(jointParams.LowerLimit)) + MaxMotorTorque = Params.Stiffness; + LimitEnabled = Params.LimitEnabled; + if (float.IsNaN(Params.LowerLimit)) { - jointParams.LowerLimit = 0; + Params.LowerLimit = 0; } - if (float.IsNaN(jointParams.UpperLimit)) + if (float.IsNaN(Params.UpperLimit)) { - jointParams.UpperLimit = 0; + Params.UpperLimit = 0; } if (ragdoll.IsFlipped) { - LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb1Anchor.X, jointParams.Limb1Anchor.Y) * jointParams.Ragdoll.JointScale); - LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb2Anchor.X, jointParams.Limb2Anchor.Y) * jointParams.Ragdoll.JointScale); - UpperLimit = MathHelper.ToRadians(-jointParams.LowerLimit); - LowerLimit = MathHelper.ToRadians(-jointParams.UpperLimit); + LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Params.Ragdoll.JointScale); + LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Params.Ragdoll.JointScale); + UpperLimit = MathHelper.ToRadians(-Params.LowerLimit); + LowerLimit = MathHelper.ToRadians(-Params.UpperLimit); } else { - LocalAnchorA = ConvertUnits.ToSimUnits(jointParams.Limb1Anchor * jointParams.Ragdoll.JointScale); - LocalAnchorB = ConvertUnits.ToSimUnits(jointParams.Limb2Anchor * jointParams.Ragdoll.JointScale); - UpperLimit = MathHelper.ToRadians(jointParams.UpperLimit); - LowerLimit = MathHelper.ToRadians(jointParams.LowerLimit); + LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Params.Ragdoll.JointScale); + LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Params.Ragdoll.JointScale); + UpperLimit = MathHelper.ToRadians(Params.UpperLimit); + LowerLimit = MathHelper.ToRadians(Params.LowerLimit); } } } partial class Limb : ISerializableEntity, ISpatialEntity { - // Note: not used - private const float LimbDensity = 15; - private const float LimbAngularDamping = 7; - //how long it takes for severed limbs to fade out private const float SeveredFadeOutTime = 10.0f; @@ -108,12 +84,12 @@ namespace Barotrauma /// Note that during the limb initialization, character.AnimController returns null, whereas this field is already assigned. /// public readonly Ragdoll ragdoll; - public readonly LimbParams limbParams; + public readonly LimbParams Params; //the physics body of the limb public PhysicsBody body; - public Vector2 StepOffset => ConvertUnits.ToSimUnits(limbParams.StepOffset) * ragdoll.RagdollParams.JointScale; + public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; public bool inWater; @@ -125,19 +101,34 @@ namespace Barotrauma private bool isSevered; private float severedFadeOutTimer; - - public Vector2? MouthPos; + + private Vector2? mouthPos; + public Vector2 MouthPos + { + get + { + if (!mouthPos.HasValue) + { + mouthPos = Params.MouthPos; + } + return mouthPos.Value; + } + set + { + mouthPos = value; + } + } public readonly Attack attack; private List damageModifiers; private Direction dir; - public int HealthIndex => limbParams.HealthIndex; - public float Scale => limbParams.Ragdoll.LimbScale; - public float AttackPriority => limbParams.AttackPriority; - public bool DoesFlip => limbParams.Flip; - public float SteerForce => limbParams.SteerForce; + public int HealthIndex => Params.HealthIndex; + public float Scale => Params.Ragdoll.LimbScale; + public float AttackPriority => Params.AttackPriority; + public bool DoesFlip => Params.Flip; + public float SteerForce => Params.SteerForce; public Vector2 DebugTargetPos; public Vector2 DebugRefPos; @@ -198,7 +189,7 @@ namespace Barotrauma set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; } } - public int RefJointIndex => limbParams.RefJoint; + public int RefJointIndex => Params.RefJoint; private List wearingItems; public List WearingItems @@ -291,7 +282,7 @@ namespace Barotrauma get { return pullJoint.LocalAnchorA; } } - public string Name => limbParams.Name; + public string Name => Params.Name; public Dictionary SerializableProperties { @@ -303,7 +294,7 @@ namespace Barotrauma { this.ragdoll = ragdoll; this.character = character; - this.limbParams = limbParams; + this.Params = limbParams; wearingItems = new List(); dir = Direction.Right; body = new PhysicsBody(limbParams); @@ -324,19 +315,16 @@ namespace Barotrauma pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale)) { Enabled = false, - MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass + //MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass + // 150 or even 400 is too low if the joint is used for moving the character position from the mainlimb towards the collider position + MaxForce = 1000 * Mass }; GameMain.World.AddJoint(pullJoint); var element = limbParams.Element; - if (element.Attribute("mouthpos") != null) - { - MouthPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("mouthpos", Vector2.Zero)); - } body.BodyType = BodyType.Dynamic; - body.FarseerBody.AngularDamping = LimbAngularDamping; damageModifiers = new List(); @@ -403,19 +391,19 @@ namespace Barotrauma return AddDamage(simPosition, afflictions, playSound); } - public AttackResult AddDamage(Vector2 simPosition, List afflictions, bool playSound) + public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound) { List appliedDamageModifiers = new List(); //create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc - afflictions = new List(afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.ApplyProbability)); - for (int i = 0; i < afflictions.Count; i++) + var afflictionsCopy = afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.Probability).ToList(); + for (int i = 0; i < afflictionsCopy.Count; i++) { foreach (DamageModifier damageModifier in damageModifiers) { - if (!damageModifier.MatchesAffliction(afflictions[i])) continue; - if (SectorHit(damageModifier.ArmorSector, simPosition)) + if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue; + if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier); + afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } @@ -424,19 +412,19 @@ namespace Barotrauma { foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { - if (!damageModifier.MatchesAffliction(afflictions[i])) continue; - if (SectorHit(damageModifier.ArmorSector, simPosition)) + if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue; + if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier); + afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } } } - AddDamageProjSpecific(simPosition, afflictions, playSound, appliedDamageModifiers); + AddDamageProjSpecific(simPosition, afflictionsCopy, playSound, appliedDamageModifiers); - return new AttackResult(afflictions, this, appliedDamageModifiers); + return new AttackResult(afflictionsCopy, this, appliedDamageModifiers); } partial void AddDamageProjSpecific(Vector2 simPosition, List afflictions, bool playSound, List appliedDamageModifiers); @@ -456,7 +444,7 @@ namespace Barotrauma protected float GetArmorSectorRotationOffset(Vector2 armorSector) { float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y); - float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation); + float spritesheetOrientation = Params.GetSpriteOrientation(); return midAngle + spritesheetOrientation; } @@ -495,10 +483,13 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime); + + private readonly List contactBodies = new List(); + private List ignoredBodies; /// /// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated. /// - public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1) + public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null) { attackResult = default(AttackResult); float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos)); @@ -514,8 +505,11 @@ namespace Barotrauma case HitDetection.Distance: if (dist < attack.DamageRange) { - List ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); - ignoredBodies.Add(character.AnimController.Collider.FarseerBody); + if (ignoredBodies == null) + { + ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); + ignoredBodies.Add(character.AnimController.Collider.FarseerBody); + } structureBody = Submarine.PickBody( SimPosition, attackSimPos, @@ -541,46 +535,42 @@ namespace Barotrauma } break; case HitDetection.Contact: - var targetBodies = new List(); + contactBodies.Clear(); if (damageTarget is Character targetCharacter) { foreach (Limb limb in targetCharacter.AnimController.Limbs) { - if (!limb.IsSevered && limb.body?.FarseerBody != null) targetBodies.Add(limb.body.FarseerBody); + if (!limb.IsSevered && limb.body?.FarseerBody != null) contactBodies.Add(limb.body.FarseerBody); } } else if (damageTarget is Structure targetStructure) { if (character.Submarine == null && targetStructure.Submarine != null) { - targetBodies.Add(targetStructure.Submarine.PhysicsBody.FarseerBody); + contactBodies.Add(targetStructure.Submarine.PhysicsBody.FarseerBody); } else { - targetBodies.AddRange(targetStructure.Bodies); + contactBodies.AddRange(targetStructure.Bodies); } } else if (damageTarget is Item) { Item targetItem = damageTarget as Item; - if (targetItem.body?.FarseerBody != null) targetBodies.Add(targetItem.body.FarseerBody); + if (targetItem.body?.FarseerBody != null) contactBodies.Add(targetItem.body.FarseerBody); } - - if (targetBodies != null) + ContactEdge contactEdge = body.FarseerBody.ContactList; + while (contactEdge != null) { - ContactEdge contactEdge = body.FarseerBody.ContactList; - while (contactEdge != null) + if (contactEdge.Contact != null && + contactEdge.Contact.IsTouching && + contactBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body)) { - if (contactEdge.Contact != null && - contactEdge.Contact.IsTouching && - targetBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body)) - { - structureBody = targetBodies.LastOrDefault(); - wasHit = true; - break; - } - contactEdge = contactEdge.Next; + structureBody = contactBodies.LastOrDefault(); + wasHit = true; + break; } + contactEdge = contactEdge.Next; } break; } @@ -601,11 +591,18 @@ namespace Barotrauma LastAttackSoundTime = SoundInterval; } #endif - attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); + if (damageTarget is Character targetCharacter && targetLimb != null) + { + attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound); + } + else + { + attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); + } if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) { // TODO: use the hit pos? - var localFront = body.GetLocalFront(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation)); + var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); var from = body.FarseerBody.GetWorldPoint(localFront); var to = from; var drawPos = body.DrawPosition; @@ -665,7 +662,7 @@ namespace Barotrauma { PhysicsBody mainLimbBody = ragdoll.MainLimb.body; Body colliderBody = ragdoll.Collider.FarseerBody; - Vector2 mainLimbLocalFront = mainLimbBody.GetLocalFront(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation)); + Vector2 mainLimbLocalFront = mainLimbBody.GetLocalFront(ragdoll.MainLimb.Params.GetSpriteOrientation()); if (Dir < 0) { mainLimbLocalFront.X = -mainLimbLocalFront.X; @@ -715,8 +712,7 @@ namespace Barotrauma public void LoadParams() { - attack?.Deserialize(); - pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(limbParams.PullPos * Scale); + pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.PullPos * Scale); LoadParamsProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/AnimationParams.cs similarity index 81% rename from Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/AnimationParams.cs rename to Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/AnimationParams.cs index 8f5506eae..9e0268f73 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/AnimationParams.cs @@ -20,30 +20,42 @@ namespace Barotrauma abstract class GroundedMovementParams : AnimationParams { - [Serialize("1.0, 1.0", true), Editable(DecimalCount = 2, ToolTip = "How big steps the character takes.")] + [Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2)] public Vector2 StepSize { get; set; } - [Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's head is positioned.")] + [Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2)] public float HeadPosition { get; set; } - [Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's torso is positioned.")] + [Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2)] public float TorsoPosition { get; set; } - [Serialize(0.75f, true), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2, ToolTip = "The character's movement speed is multiplied with this value when moving backwards.")] + [Serialize(1f, true, 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)] + public float StepLiftAmount { 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)] + 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)] + 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)] public float BackwardsMovementMultiplier { get; set; } } abstract class SwimParams : AnimationParams { - [Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [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 = 500)] public float SteerTorque { get; set; } } - abstract class AnimationParams : EditableParams + abstract class AnimationParams : EditableParams, IMemorizable { public string SpeciesName { get; private set; } public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run; @@ -51,11 +63,11 @@ namespace Barotrauma protected static Dictionary> allAnimations = new Dictionary>(); - [Serialize(1.0f, true), Editable(DecimalCount = 2)] + [Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED)] public float MovementSpeed { get; set; } - [Serialize(1.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, - ToolTip = "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, 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)"), + Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float CycleSpeed { get; set; } /// @@ -101,8 +113,13 @@ namespace Barotrauma public static string GetFolder(string speciesName, ContentPackage contentPackage = null) { - string configFilePath = Character.GetConfigFile(speciesName, contentPackage); - var folder = XMLExtensions.TryLoadXml(configFilePath)?.Root?.Element("animations")?.GetAttributeString("folder", string.Empty); + string configFilePath = Character.GetConfigFilePath(speciesName, contentPackage); + if (!Character.TryGetConfigFile(configFilePath, out XDocument configFile)) + { + DebugConsole.ThrowError($"Failed to load config file: {configFilePath} for '{speciesName}'"); + return string.Empty; + } + var folder = configFile.Root?.Element("animations")?.GetAttributeString("folder", string.Empty); if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default") { folder = Path.Combine(Path.GetDirectoryName(configFilePath), "Animations"); @@ -197,9 +214,10 @@ namespace Barotrauma T a = new T(); if (a.Load(selectedFile, speciesName)) { - if (!anims.ContainsKey(a.Name)) + fileName = Path.GetFileNameWithoutExtension(selectedFile); + if (!anims.ContainsKey(fileName)) { - anims.Add(a.Name, a); + anims.Add(fileName, a); } } else @@ -211,6 +229,8 @@ namespace Barotrauma return (T)anim; } + public static void ClearCache() => allAnimations.Clear(); + public static AnimationParams Create(string fullPath, string speciesName, AnimationType animationType, Type type) { if (type == typeof(HumanWalkParams)) @@ -275,11 +295,14 @@ namespace Barotrauma instance.IsLoaded = instance.Deserialize(animationElement); instance.Save(); instance.Load(fullPath, speciesName); - anims.Add(instance.Name, instance); + anims.Add(fileName, instance); DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite); return instance as T; } + public bool Serialize() => base.Serialize(); + public bool Deserialize() => base.Deserialize(); + protected bool Load(string file, string speciesName) { if (Load(file)) @@ -380,14 +403,16 @@ namespace Barotrauma } #region Memento - protected void CreateSnapshot() where T : AnimationParams, new() + public Memento Memento { get; protected set; } = new Memento(); + public abstract void StoreSnapshot(); + protected void StoreSnapshot() where T : AnimationParams, new() { - Serialize(); if (doc == null) { DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!"); return; } + Serialize(); var copy = new T { IsLoaded = true, @@ -395,10 +420,11 @@ namespace Barotrauma }; copy.Deserialize(); copy.Serialize(); - memento.Store(copy); + Memento.Store(copy); } - public override void Undo() => Deserialize(memento.Undo().MainElement); - public override void Redo() => Deserialize(memento.Redo().MainElement); + public void Undo() => Deserialize(Memento.Undo().MainElement); + public void Redo() => Deserialize(Memento.Redo().MainElement); + public void ClearHistory() => Memento.Clear(); #endregion } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/FishAnimations.cs similarity index 66% rename from Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/FishAnimations.cs rename to Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/FishAnimations.cs index daf93bc99..176312c13 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/FishAnimations.cs @@ -16,7 +16,7 @@ namespace Barotrauma protected static FishWalkParams Empty = new FishWalkParams(); - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } class FishRunParams : FishGroundedParams @@ -32,7 +32,7 @@ namespace Barotrauma protected static FishRunParams Empty = new FishRunParams(); - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } class FishSwimFastParams : FishSwimParams @@ -43,7 +43,7 @@ namespace Barotrauma return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); } - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } class FishSwimSlowParams : FishSwimParams @@ -54,7 +54,7 @@ namespace Barotrauma return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); } - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } abstract class FishGroundedParams : GroundedMovementParams, IFishAnimation @@ -69,38 +69,38 @@ namespace Barotrauma return true; } - [Serialize(true, true), Editable(ToolTip = "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, 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.")] public bool Flip { get; set; } - [Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the head to the correct position.")] + [Serialize(10.0f, true, 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), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the torso to the correct position.")] + [Serialize(10.0f, true, 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), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")] + [Serialize(8.0f, true, 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), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")] + [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float HeadTorque { get; set; } - [Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the torso to the correct orientation.")] + [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float TorsoTorque { get; set; } - [Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")] + [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float TailTorque { get; set; } - [Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")] + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float FootTorque { get; set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "Optional torque that's constantly applied to legs.")] + [Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float LegTorque { get; set; } /// /// The angle of the collider when standing (i.e. out of water). /// In degrees. /// - [Serialize(0f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "The angle of the character's collider when standing.")] + [Serialize(0f, true, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] public float ColliderStandAngle { get => MathHelper.ToDegrees(ColliderStandAngleInRadians); @@ -140,13 +140,13 @@ namespace Barotrauma abstract class FishSwimParams : SwimParams, IFishAnimation { - [Serialize(false, true), Editable(ToolTip = "TODO")] + [Serialize(false, true, description: "TODO"), Editable] public bool UseSineMovement { get; set; } - [Serialize(true, true), Editable(ToolTip = "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, 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.")] public bool Flip { get; set; } - [Serialize(true, true), Editable(ToolTip = "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, 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.")] public bool Mirror { get; set; } [Serialize(1f, true), Editable] @@ -155,19 +155,19 @@ namespace Barotrauma [Serialize(10.0f, true), Editable] public float WaveLength { get; set; } - [Serialize(true, true), Editable(ToolTip = "Should the character face towards the direction it's heading.")] + [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the torso to the correct orientation.")] + [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float TorsoTorque { get; set; } - - [Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")] + + [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float HeadTorque { get; set; } - [Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")] + [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float TailTorque { get; set; } - [Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")] + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float FootTorque { get; set; } [Serialize(null, true), Editable] diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/HumanoidAnimations.cs similarity index 61% rename from Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/HumanoidAnimations.cs rename to Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/HumanoidAnimations.cs index 9ff43844c..58d4dcded 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/HumanoidAnimations.cs @@ -10,7 +10,7 @@ namespace Barotrauma return GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName); } - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } class HumanRunParams : HumanGroundedParams @@ -21,7 +21,7 @@ namespace Barotrauma return GetAnimParams(character.SpeciesName, AnimationType.Run, fileName); } - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } class HumanSwimFastParams: HumanSwimParams @@ -33,7 +33,7 @@ namespace Barotrauma } - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } class HumanSwimSlowParams : HumanSwimParams @@ -44,7 +44,7 @@ namespace Barotrauma return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); } - public override void CreateSnapshot() => CreateSnapshot(); + public override void StoreSnapshot() => StoreSnapshot(); } abstract class HumanSwimParams : SwimParams, IHumanAnimation @@ -81,44 +81,44 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")] + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootRotateStrength { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation { - [Serialize(0.3f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2, ToolTip = "How much force is used to force the character upright.")] + [Serialize(0.3f, true, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float GetUpForce { get; set; } // -- TODO: use a separate clip for crawling -> replace these when implemented. - [Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the torso when crouching.")] + [Serialize(0.65f, true, description: "Height of the torso when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)] public float CrouchingTorsoPos { get; set; } - [Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the head when crouching.")] + [Serialize(0.65f, true, description: "Height of the head when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)] public float CrouchingHeadPos { get; set; } /// /// In degrees /// - [Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the torso when crouching.")] + [Serialize(-10f, true, description: "Angle of the torso when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] public float CrouchingTorsoAngle { get; set; } /// /// In degrees /// - [Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the head when crouching.")] + [Serialize(-10f, true, description: "Angle of the head when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] public float CrouchingHeadAngle { get; set; } // -- - [Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's head leans forwards when moving.")] + [Serialize(0.25f, true, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)] public float HeadLeanAmount { get; set; } - [Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's torso leans forwards when moving.")] + [Serialize(0.25f, true, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)] public float TorsoLeanAmount { get; set; } - [Serialize(15.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")] + [Serialize(15.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveStrength { get; set; } /// @@ -135,28 +135,28 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(20.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")] + [Serialize(20.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootRotateStrength { get; set; } - [Serialize("0.0, 0.0", true), Editable(DecimalCount = 2, ToolTip = "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.")] + [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)] public Vector2 FootMoveOffset { get; set; } - [Serialize("0.0, 0.0", true), Editable(DecimalCount = 2, ToolTip = "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.")] + [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)] public Vector2 CrouchingFootMoveOffset { get; set; } - [Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to bend the characters legs when taking a step.")] + [Serialize(10.0f, true, 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), Editable(DecimalCount = 2, ToolTip = "How much the hands move along each axis.")] + [Serialize("0.4, 0.15", true, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } - [Serialize("-0.15, 0.0", true), Editable(DecimalCount = 2, ToolTip = "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.")] + [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)] public Vector2 HandMoveOffset { get; set; } - [Serialize(0.7f, true), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2, ToolTip = "How much force is used to move the hands.")] + [Serialize(0.7f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] public float HandMoveStrength { get; set; } - [Serialize(-1.0f, true), Editable(DecimalCount = 2, ToolTip = "The position of the hands is clamped below this (relative to the position of the character's torso).")] + [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)] public float HandClampY { get; set; } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/CharacterParams.cs new file mode 100644 index 000000000..931a794ec --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/CharacterParams.cs @@ -0,0 +1,602 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Xml; +using System.Linq; +using Barotrauma.Extensions; +#if CLIENT +using SoundType = Barotrauma.CharacterSound.SoundType; +#endif + +namespace Barotrauma +{ + /// + /// Contains character data that should be editable in the character editor. + /// + class CharacterParams : EditableParams + { + [Serialize("", true), Editable] + public string SpeciesName { 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(false, true), Editable] + public bool Humanoid { get; private set; } + + [Serialize(false, true), Editable] + public bool Husk { get; private set; } + + [Serialize(false, true), Editable] + public bool NeedsAir { get; set; } + + [Serialize(false, true), Editable] + public bool CanSpeak { get; set; } + + [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)] + public float Noise { get; set; } + + [Serialize("blood", true), Editable] + public string BloodDecal { get; private set; } + + public readonly string File; + + public readonly List SubParams = new List(); + public readonly List Sounds = new List(); + public readonly List BloodEmitters = new List(); + public readonly List GibEmitters = new List(); + public readonly List Inventories = new List(); + public HealthParams Health { get; private set; } + public AIParams AI { get; private set; } + + public CharacterParams(string file) + { + File = file; + Load(); + } + + protected override string GetName() => "Character Config File"; + + public override XElement MainElement => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + + public bool Load() + { + bool success = base.Load(File); + if (string.IsNullOrEmpty(SpeciesName) && MainElement != null) + { + //backwards compatibility + SpeciesName = MainElement.GetAttributeString("name", ""); + } + CreateSubParams(); + return success; + } + + public bool Save(string fileNameWithoutExtension = null) + { + Serialize(); + return base.Save(fileNameWithoutExtension, new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = false + }); + } + + public override bool Reset(bool forceReload = false) + { + if (forceReload) + { + return Load(); + } + Deserialize(OriginalElement, alsoChildren: true); + SubParams.ForEach(sp => sp.Reset()); + return true; + } + + public bool CompareGroup(string group) => !string.IsNullOrWhiteSpace(group) && !string.IsNullOrWhiteSpace(Group) && group.Equals(Group, StringComparison.OrdinalIgnoreCase); + + protected void CreateSubParams() + { + SubParams.Clear(); + var health = MainElement.GetChildElement("health"); + if (health != null) + { + Health = new HealthParams(health, this); + SubParams.Add(Health); + } + // TODO: support for multiple ai elements? + var ai = MainElement.GetChildElement("ai"); + if (ai != null) + { + AI = new AIParams(ai, this); + SubParams.Add(AI); + } + foreach (var element in MainElement.GetChildElements("bloodemitter")) + { + var emitter = new ParticleParams(element, this); + BloodEmitters.Add(emitter); + SubParams.Add(emitter); + } + foreach (var element in MainElement.GetChildElements("gibemitter")) + { + var emitter = new ParticleParams(element, this); + GibEmitters.Add(emitter); + SubParams.Add(emitter); + } + foreach (var soundElement in MainElement.GetChildElements("sound")) + { + var sound = new SoundParams(soundElement, this); + Sounds.Add(sound); + SubParams.Add(sound); + } + foreach (var inventoryElement in MainElement.GetChildElements("inventory")) + { + var inventory = new InventoryParams(inventoryElement, this); + Inventories.Add(inventory); + SubParams.Add(inventory); + } + } + + public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true) + { + if (base.Deserialize(element)) + { + //backwards compatibility + if (string.IsNullOrEmpty(SpeciesName)) + { + SpeciesName = element.GetAttributeString("name", "[NAME NOT GIVEN]"); + } + if (alsoChildren) + { + SubParams.ForEach(p => p.Deserialize(recursive)); + } + return true; + } + return false; + } + + public bool Serialize(XElement element = null, bool alsoChildren = true, bool recursive = true) + { + if (base.Serialize(element)) + { + if (alsoChildren) + { + SubParams.ForEach(p => p.Serialize(recursive)); + } + return true; + } + return false; + } + +#if CLIENT + public void AddToEditor(ParamsEditor editor, bool alsoChildren = true, bool recursive = true, int space = 0) + { + base.AddToEditor(editor); + if (alsoChildren) + { + SubParams.ForEach(s => s.AddToEditor(editor, recursive)); + } + if (space > 0) + { + new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) + { + CanBeFocused = false + }; + } + } +#endif + + public bool AddSound() => TryAddSubParam(new XElement("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 AddBloodEmitter() => AddEmitter("bloodemitter"); + public void AddGibEmitter() => AddEmitter("gibemitter"); + + private void AddEmitter(string type) + { + switch (type) + { + case "gibemitter": + TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters); + break; + case "bloodemitter": + TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters); + break; + default: throw new NotImplementedException(type); + } + } + + public bool RemoveSound(SoundParams soundParams) => RemoveSubParam(soundParams); + public bool RemoveBloodEmitter(ParticleParams emitter) => RemoveSubParam(emitter, BloodEmitters); + public bool RemoveGibEmitter(ParticleParams emitter) => RemoveSubParam(emitter, GibEmitters); + public bool RemoveInventory(InventoryParams inventory) => RemoveSubParam(inventory, Inventories); + + protected bool RemoveSubParam(T subParam, IList collection = null) where T : SubParam + { + if (subParam == null || subParam.Element == null || subParam.Element.Parent == null) { return false; } + if (collection != null && !collection.Contains(subParam)) { return false; } + if (!SubParams.Contains(subParam)) { return false; } + collection?.Remove(subParam); + SubParams.Remove(subParam); + subParam.Element.Remove(); + return true; + } + + protected bool TryAddSubParam(XElement 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) + { + if (filter(collection)) { return false; } + } + MainElement.Add(element); + SubParams.Add(subParam); + collection?.Add(subParam); + return subParam != null; + } + + #region Subparams + public class SoundParams : SubParam + { + public override string Name => "Sound"; + + [Serialize("", true), Editable] + public string File { get; private set; } + +#if CLIENT + [Serialize(SoundType.Idle, true), Editable] + public SoundType State { get; private set; } +#endif + + [Serialize(1000f, true), Editable(minValue: 0f, maxValue: 10000f)] + public float Range { get; private set; } + + [Serialize(1.0f, true), 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; } + + public SoundParams(XElement element, CharacterParams character) : base(element, character) { } + } + + public class ParticleParams : SubParam + { + private string name; + public override string Name + { + get + { + if (name == null && Element != null) + { + name = Element.Name.ToString().FormatCamelCaseWithSpaces(); + } + return name; + } + } + + [Serialize("", true), Editable] + public string Particle { get; set; } + + [Serialize(0f, true), Editable(-360f, 360f, decimals: 0)] + public float AngleMin { get; private set; } + + [Serialize(0f, true), Editable(-360f, 360f, decimals: 0)] + public float AngleMax { get; private set; } + + [Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)] + public float ScaleMin { get; private set; } + + [Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)] + public float ScaleMax { get; private set; } + + [Serialize(0f, true), Editable(0f, 10000f, decimals: 0)] + public float VelocityMin { get; private set; } + + [Serialize(0f, true), Editable(0f, 10000f, decimals: 0)] + public float VelocityMax { get; private set; } + + [Serialize(0f, true), Editable(0f, 100f, decimals: 2)] + public float EmitInterval { get; private set; } + + [Serialize(0, true), Editable(0, 1000)] + public int ParticlesPerSecond { get; private set; } + + [Serialize(0, true), Editable(0, 1000)] + public int ParticleAmount { get; private set; } + + [Serialize(false, true), Editable] + public bool HighQualityCollisionDetection { get; private set; } + + [Serialize(false, true), Editable] + public bool CopyEntityAngle { get; private set; } + + public ParticleParams(XElement 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)] + public float Vitality { get; set; } + + [Serialize(true, true), Editable] + public bool DoesBleed { get; set; } + + [Serialize(float.NegativeInfinity, true), Editable(minValue: float.NegativeInfinity, maxValue: 0)] + public float CrushDepth { get; set; } + + // Make editable? + [Serialize(false, true)] + 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 = 10)] + 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 = 10)] + public float BurnReduction { get; private set; } + + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)] + public float ConstantHealthRegeneration { get; private set; } + + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)] + public float HealthRegenerationWhenEating { get; private set; } + + // TODO: limbhealths, sprite? + + public HealthParams(XElement element, CharacterParams character) : base(element, character) { } + } + + public class InventoryParams : SubParam + { + public class InventoryItem : SubParam + { + public override string Name => "Item"; + + [Serialize("", true, description: "Item identifier."), Editable()] + public string Identifier { get; private set; } + + public InventoryItem(XElement 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()] + public string Slots { get; private set; } + + [Serialize(false, true), 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)] + public float Commonness { get; private set; } + + public List Items { get; private set; } = new List(); + + public InventoryParams(XElement element, CharacterParams character) : base(element, character) + { + foreach (var itemElement in element.GetChildElements("item")) + { + var item = new InventoryItem(itemElement, character); + SubParams.Add(item); + Items.Add(item); + } + } + + public void AddItem(string identifier = null) + { + identifier = identifier ?? ""; + var element = new XElement("item", new XAttribute("identifier", identifier)); + Element.Add(element); + var item = new InventoryItem(element, Character); + SubParams.Add(item); + Items.Add(item); + } + + public bool RemoveItem(InventoryItem item) => RemoveSubParam(item, Items); + } + + public class AIParams : SubParam + { + public override string Name => "AI"; + + [Serialize(1.0f, true, 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)] + 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)] + public float Hearing { get; private set; } + + [Serialize(100f, true, description: "How much the target priority increase when the character takes damage? Additive."), Editable(minValue: -1000f, maxValue: 1000f)] + public float AggressionHurt { get; private set; } + + [Serialize(10f, true, description: "How much the target priority increase when the character takes damage? Additive."), 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)] + public float FleeHealthThreshold { get; private set; } + + [Serialize(false, true, description: "Does the character attack ONLY when provoked?"), Editable()] + public bool AttackOnlyWhenProvoked { get; private set; } + + [Serialize(true, true, description: "When true, the character retaliates quickly when it's taking damage. Enabled by default."), Editable] + public bool RetaliateWhenTakingDamage { get; private set; } + + [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()] + public bool AggressiveBoarding { get; private set; } + + // TODO: latchonto, swarming + + public IEnumerable Targets => targets; + protected readonly List targets = new List(); + + public AIParams(XElement element, CharacterParams character) : base(element, character) + { + element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _)); + element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _)); + } + + private bool TryAddTarget(XElement targetElement, out TargetParams target) + { + string tag = targetElement.GetAttributeString("tag", null); + if (!CheckTag(tag)) + { + target = null; + DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); + return false; + } + else + { + target = new TargetParams(targetElement, Character); + targets.Add(target); + SubParams.Add(target); + return true; + } + } + + 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) + { + var element = TargetParams.CreateNewElement(tag, state, priority); + if (TryAddTarget(element, out targetParams)) + { + Element.Add(element); + } + return targetParams != null; + } + + private bool CheckTag(string tag) + { + if (tag == null) { return false; } + tag = tag.ToLowerInvariant(); + return targets.None(t => t.Tag == tag); + } + + public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets); + + public bool TryGetTarget(string targetTag, out TargetParams target) + { + target = targets.FirstOrDefault(t => t.Tag == targetTag); + return target != null; + } + + public TargetParams GetTarget(string targetTag, bool throwError = true) + { + if (!TryGetTarget(targetTag, out TargetParams target)) + { + if (throwError) + { + DebugConsole.ThrowError($"Cannot find a target with the tag {targetTag}!"); + } + } + return target; + } + } + + public class TargetParams : SubParam + { + 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()] + public string Tag { get; private set; } + + [Serialize(AIState.Idle, true), Editable] + public AIState State { get; set; } + + [Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f)] + public float Priority { get; set; } + + public TargetParams(XElement element, CharacterParams character) : base(element, character) { } + + public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { } + + public static XElement CreateNewElement(string tag, AIState state, float priority) + { + return new XElement("target", + new XAttribute("tag", tag), + new XAttribute("state", state), + new XAttribute("priority", priority)); + } + } + + public abstract class SubParam : ISerializableEntity + { + public virtual string Name { get; set; } + public Dictionary SerializableProperties { get; private set; } + public XElement Element { get; set; } + public List SubParams { get; set; } = new List(); + + public CharacterParams Character { get; private set; } + + public SubParam(XElement element, CharacterParams character) + { + Element = element; + Character = character; + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public virtual bool Deserialize(bool recursive = true) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, Element); + if (recursive) + { + SubParams.ForEach(sp => sp.Deserialize(true)); + } + return SerializableProperties != null; + } + + public virtual bool Serialize(bool recursive = true) + { + SerializableProperty.SerializeProperties(this, Element, true); + if (recursive) + { + SubParams.ForEach(sp => sp.Serialize(true)); + } + return true; + } + + public virtual void Reset() + { + // Don't use recursion, because the reset method might be overriden + Deserialize(false); + SubParams.ForEach(sp => sp.Reset()); + } + + protected bool RemoveSubParam(T subParam, IList collection = null) where T : SubParam + { + if (subParam == null || subParam.Element == null || subParam.Element.Parent == null) { return false; } + if (collection != null && !collection.Contains(subParam)) { return false; } + if (!SubParams.Contains(subParam)) { return false; } + collection?.Remove(subParam); + SubParams.Remove(subParam); + subParam.Element.Remove(); + return true; + } + +#if CLIENT + public SerializableEntityEditor SerializableEntityEditor { get; protected set; } + public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, ScalableFont titleFont = null) + { + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUI.LargeFont); + if (recursive) + { + SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUI.SmallFont)); + } + if (space > 0) + { + new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: new Color(20, 20, 20, 255)) + { + CanBeFocused = false + }; + } + } +#endif + } + #endregion + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/EditableParams.cs similarity index 84% rename from Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/EditableParams.cs rename to Barotrauma/BarotraumaShared/Source/Characters/Params/EditableParams.cs index 9155d1d77..66bf5f3c3 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/EditableParams.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Xml; using System.Xml.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -32,9 +33,11 @@ namespace Barotrauma } } - public XElement MainElement => doc.Root; + public virtual XElement MainElement => doc.Root; public XElement OriginalElement { get; protected set; } + protected virtual string GetName() => Path.GetFileNameWithoutExtension(FullPath).FormatCamelCaseWithSpaces(); + protected virtual bool Deserialize(XElement element = null) { element = element ?? MainElement; @@ -67,7 +70,7 @@ namespace Barotrauma protected virtual void UpdatePath(string fullPath) { FullPath = fullPath; - Name = Path.GetFileNameWithoutExtension(FullPath); + Name = GetName(); FileName = Path.GetFileName(FullPath); Folder = Path.GetDirectoryName(FullPath); } @@ -112,23 +115,22 @@ namespace Barotrauma #if CLIENT public SerializableEntityEditor SerializableEntityEditor { get; protected set; } - public virtual void AddToEditor(ParamsEditor editor) + public virtual void AddToEditor(ParamsEditor editor, int space = 0) { if (!IsLoaded) { DebugConsole.ThrowError("[Params] Not loaded!"); return; } - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUI.LargeFont); + if (space > 0) + { + new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) + { + CanBeFocused = false + }; + } } #endif - - #region Memento - public readonly Memento memento = new Memento(); - public abstract void CreateSnapshot(); - public abstract void Undo(); - public abstract void Redo(); - public void ClearHistory() => memento.Clear(); - #endregion } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/Ragdoll/RagdollParams.cs new file mode 100644 index 000000000..47fcd7ca1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/Ragdoll/RagdollParams.cs @@ -0,0 +1,1151 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using System.IO; +using System.Xml; +using Barotrauma.Extensions; +#if CLIENT +using Barotrauma.SpriteDeformations; +#endif + +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); + } + + class FishRagdollParams : RagdollParams + { + public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); + } + + class RagdollParams : EditableParams, IMemorizable + { + #region Ragdoll + public const float MIN_SCALE = 0.1f; + public const float MAX_SCALE = 2; + + public string 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] + public string Texture { get; set; } + + [Serialize(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'. Used mainly for animations and widgets."), Editable(-360, 360)] + public float SpritesheetOrientation { get; set; } + + private float limbScale; + [Serialize(1.0f, true), 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)] + 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)] + 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)] + 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)] + public float ImpactTolerance { get; set; } + + [Serialize(true, true, description: "Can the creature enter submarine and walk when there is no water? Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] + public bool CanEnterSubmarine { get; set; } + + [Serialize(true, true, description: "Can the character be dragged around by other creatures?"), Editable()] + public bool Draggable { get; set; } + + private 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))); + + 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 GetFolder(string speciesName, ContentPackage contentPackage = null) + { + string configFilePath = Character.GetConfigFilePath(speciesName, contentPackage); + if (!Character.TryGetConfigFile(configFilePath, out XDocument configFile)) + { + DebugConsole.ThrowError($"Failed to load config file: {configFilePath} for '{speciesName}'"); + return string.Empty; + } + var folder = configFile.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty); + if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default") + { + folder = Path.Combine(Path.GetDirectoryName(configFilePath), "Ragdolls") + Path.DirectorySeparatorChar; + } + return folder; + } + + public static T GetDefaultRagdollParams(string 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() + { + if (string.IsNullOrWhiteSpace(speciesName)) + { + throw new Exception($"Species name null or empty!"); + } + if (!allRagdolls.TryGetValue(speciesName, out Dictionary ragdolls)) + { + ragdolls = new Dictionary(); + allRagdolls.Add(speciesName, ragdolls); + } + if (string.IsNullOrEmpty(fileName) || !ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) + { + string selectedFile = null; + string folder = GetFolder(speciesName); + if (Directory.Exists(folder)) + { + var files = Directory.GetFiles(folder); + 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).ToLowerInvariant() == fileName.ToLowerInvariant()); + if (selectedFile == null) + { + DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."); + selectedFile = GetDefaultFile(speciesName); + } + } + } + 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)) + { + 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}."); + } + } + return (T)ragdoll; + } + + /// + /// 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() + { + // Remove the old ragdolls, if found. + if (allRagdolls.ContainsKey(speciesName)) + { + DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red); + allRagdolls.Remove(speciesName); + } + var ragdolls = new Dictionary(); + allRagdolls.Add(speciesName, ragdolls); + var instance = new T + { + doc = new XDocument(mainElement) + }; + instance.UpdatePath(fullPath); + instance.IsLoaded = instance.Deserialize(mainElement); + instance.Save(); + instance.Load(fullPath, speciesName); + ragdolls.Add(instance.Name, instance); + DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite); + return instance as T; + } + + public static void ClearCache() => allRagdolls.Clear(); + + protected override void UpdatePath(string fullPath) + { + if (SpeciesName == null) + { + base.UpdatePath(fullPath); + } + else + { + // Update the key by removing and re-adding the ragdoll. + if (allRagdolls.TryGetValue(SpeciesName, out Dictionary ragdolls)) + { + ragdolls.Remove(Name); + } + base.UpdatePath(fullPath); + if (ragdolls != null) + { + if (!ragdolls.ContainsKey(Name)) + { + ragdolls.Add(Name, this); + } + } + } + } + + public bool Save(string fileNameWithoutExtension = null) + { + OriginalElement = MainElement; + GetAllSubParams().ForEach(p => p.SetCurrentElementAsOriginalElement()); + Serialize(); + return base.Save(fileNameWithoutExtension, new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = false + }); + } + + protected bool Load(string file, string speciesName) + { + if (Load(file)) + { + SpeciesName = speciesName; + CreateColliders(); + CreateLimbs(); + CreateJoints(); + return true; + } + return false; + } + + /// + /// Applies the current properties to the xml definition without saving to file. + /// + public void Apply() + { + Serialize(); + } + + /// + /// Resets the current properties to the xml (stored in memory). Force reload reloads the file from disk. + /// + public override bool Reset(bool forceReload = false) + { + if (forceReload) + { + return Load(FullPath, SpeciesName); + } + // Don't use recursion, because the reset method might be overriden + Deserialize(OriginalElement, alsoChildren: false, recursive: false); + GetAllSubParams().ForEach(sp => sp.Reset()); + return true; + } + + protected void CreateColliders() + { + Colliders.Clear(); + for (int i = 0; i < MainElement.GetChildElements("collider").Count(); i++) + { + var element = MainElement.GetChildElements("collider").ElementAt(i); + string name = i > 0 ? "Secondary Collider" : "Main Collider"; + Colliders.Add(new ColliderParams(element, this, name)); + } + } + + protected void CreateLimbs() + { + Limbs.Clear(); + foreach (var element in MainElement.GetChildElements("limb")) + { + Limbs.Add(new LimbParams(element, this)); + } + Limbs = Limbs.OrderBy(l => l.ID).ToList(); + } + + protected void CreateJoints() + { + Joints.Clear(); + foreach (var element in MainElement.GetChildElements("joint")) + { + Joints.Add(new JointParams(element, this)); + } + } + + public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true) + { + if (base.Deserialize(element)) + { + if (alsoChildren) + { + GetAllSubParams().ForEach(p => p.Deserialize(recursive: recursive)); + } + return true; + } + return false; + } + + public bool Serialize(XElement element = null, bool alsoChildren = true, bool recursive = true) + { + if (base.Serialize(element)) + { + if (alsoChildren) + { + GetAllSubParams().ForEach(p => p.Serialize(recursive: recursive)); + } + return true; + } + return false; + } + +#if CLIENT + public void AddToEditor(ParamsEditor editor, bool alsoChildren = true, int space = 0) + { + base.AddToEditor(editor); + if (alsoChildren) + { + var subParams = GetAllSubParams(); + foreach (var subParam in subParams) + { + subParam.AddToEditor(editor, true, space); + } + } + if (space > 0) + { + new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) + { + CanBeFocused = false + }; + } + } +#endif + #endregion + + #region Memento + public Memento Memento { get; protected set; } = new Memento(); + public void StoreSnapshot() + { + Serialize(); + if (doc == null) + { + DebugConsole.ThrowError("[RagdollParams] The source XML Document is null!"); + return; + } + var copy = new RagdollParams + { + IsLoaded = true, + doc = new XDocument(doc) + }; + copy.CreateColliders(); + copy.CreateLimbs(); + copy.CreateJoints(); + copy.Deserialize(); + copy.Serialize(); + Memento.Store(copy); + } + public void Undo() => RevertTo(Memento.Undo() as RagdollParams); + public void Redo() => RevertTo(Memento.Redo() as RagdollParams); + public void ClearHistory() => Memento.Clear(); + + private void RevertTo(RagdollParams source) + { + if (source.MainElement == null) + { + DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!"); + return; + } + Deserialize(source.MainElement, alsoChildren: false); + var sourceSubParams = source.GetAllSubParams().ToList(); + var subParams = GetAllSubParams().ToList(); + // TODO: cannot currently undo joint/limb deletion. + if (sourceSubParams.Count != subParams.Count) + { + DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); + return; + } + for (int i = 0; i < subParams.Count; i++) + { + var subSubParams = subParams[i].SubParams; + if (subSubParams.Count != sourceSubParams[i].SubParams.Count) + { + DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); + return; + } + subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false); + for (int j = 0; j < subSubParams.Count; j++) + { + subSubParams[j].Deserialize(sourceSubParams[i].SubParams[j].Element, recursive: false); + // Since we cannot use recursion here, we have to go deeper manually, if necessary. + } + } + } + #endregion + + #region Subparams + public class JointParams : SubParam + { + private string name; + [Serialize("", true), Editable] + public override string Name + { + get + { + if (string.IsNullOrWhiteSpace(name)) + { + name = GenerateName(); + } + return name; + } + set + { + name = value; + } + } + + public override string GenerateName() => $"Joint {Limb1} - {Limb2}"; + + [Serialize(-1, true), Editable] + public int Limb1 { get; set; } + + [Serialize(-1, true), 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()] + public Vector2 Limb1Anchor { get; set; } + + /// + /// Should be converted to sim units. + /// + [Serialize("1.0, 1.0", true, description: "Local position of the join in the Limb2."), Editable()] + public Vector2 Limb2Anchor { get; set; } + + [Serialize(true, true), Editable] + public bool CanBeSevered { get; set; } + + [Serialize(true, true), Editable] + public bool LimitEnabled { get; set; } + + /// + /// In degrees. + /// + [Serialize(0f, true), Editable] + public float UpperLimit { get; set; } + + /// + /// In degrees. + /// + [Serialize(0f, true), Editable] + public float LowerLimit { get; set; } + + [Serialize(0.25f, true), Editable] + public float Stiffness { get; set; } + + public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + } + + public class LimbParams : SubParam + { + public readonly SpriteParams normalSpriteParams; + public readonly SpriteParams damagedSpriteParams; + public readonly DeformSpriteParams deformSpriteParams; + public readonly List decorativeSpriteParams = new List(); + + public AttackParams Attack { get; private set; } + public SoundParams Sound { get; private set; } + public LightSourceParams LightSource { get; private set; } + public List DamageModifiers { get; private set; } = new List(); + + private string name; + [Serialize("", true), Editable] + public override string Name + { + get + { + if (string.IsNullOrWhiteSpace(name)) + { + name = GenerateName(); + } + return name; + } + set + { + name = value; + } + } + + public override string GenerateName() => $"Limb {ID}"; + + public SpriteParams GetSprite() => deformSpriteParams ?? normalSpriteParams; + + [Serialize(-1, true), 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()] + public LimbType Type { 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. Used mainly for animations and widgets."), Editable(-360, 360)] + public float SpriteOrientation { get; set; } + + public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation); + + [Serialize(true, true, 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()] + public bool MirrorVertically { get; set; } + + [Serialize(false, true), Editable] + public bool MirrorHorizontally { get; set; } + + [Serialize(false, true, description: "Disable drawing for this limb."), Editable()] + public bool Hide { get; set; } + + [Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable()] + public float AttackPriority { get; set; } + + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + public float SteerForce { get; set; } + + [Serialize(0f, true, 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)] + public float Height { get; set; } + + [Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + public float Width { get; set; } + + [Serialize(10f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + public float Density { get; set; } + + [Serialize(false, true), Editable] + public bool IgnoreCollisions { get; set; } + + [Serialize(7f, true), Editable] + public float AngularDamping { 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()] + 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()] + 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()] + 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)] + public Vector2 MouthPos { get; set; } + + [Serialize("", true), Editable] + public string Notes { get; set; } + + // Non-editable -> + [Serialize(0, true)] + public int HealthIndex { get; set; } + + [Serialize(0.3f, true)] + public float Friction { get; set; } + + [Serialize(0.05f, true)] + public float Restitution { get; set; } + + public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + { + var spriteElement = element.GetChildElement("sprite"); + if (spriteElement != null) + { + normalSpriteParams = new SpriteParams(spriteElement, ragdoll); + SubParams.Add(normalSpriteParams); + } + var damagedSpriteElement = element.GetChildElement("damagedsprite"); + if (damagedSpriteElement != null) + { + damagedSpriteParams = new SpriteParams(damagedSpriteElement, ragdoll); + // Hide the damaged sprite params in the editor for now. + //SubParams.Add(damagedSpriteParams); + } + var deformSpriteElement = element.GetChildElement("deformablesprite"); + if (deformSpriteElement != null) + { + deformSpriteParams = new DeformSpriteParams(deformSpriteElement, ragdoll); + SubParams.Add(deformSpriteParams); + } + foreach (var decorativeSpriteElement in element.GetChildElements("decorativesprite")) + { + var decorativeParams = new DecorativeSpriteParams(decorativeSpriteElement, ragdoll); + decorativeSpriteParams.Add(decorativeParams); + SubParams.Add(decorativeParams); + } + var attackElement = element.GetChildElement("attack"); + if (attackElement != null) + { + Attack = new AttackParams(attackElement, ragdoll); + SubParams.Add(Attack); + } + foreach (var damageElement in element.GetChildElements("damagemodifier")) + { + var damageModifier = new DamageModifierParams(damageElement, ragdoll); + DamageModifiers.Add(damageModifier); + SubParams.Add(damageModifier); + } + var soundElement = element.GetChildElement("sound"); + if (soundElement != null) + { + Sound = new SoundParams(soundElement, ragdoll); + SubParams.Add(Sound); + } + var lightElement = element.GetChildElement("lightsource"); + if (lightElement != null) + { + LightSource = new LightSourceParams(lightElement, ragdoll); + SubParams.Add(LightSource); + } + } + + public bool AddAttack() + { + if (Attack != null) { return false; } + TryAddSubParam(new XElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack); + Attack = newAttack; + return Attack != null; + } + + + public bool AddSound() + { + if (Sound != null) { return false; } + TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound); + Sound = newSound; + return Sound != null; + } + + public bool AddLight() + { + if (LightSource != null) { return false; } + var lightSourceElement = new XElement("lightsource", + new XElement("lighttexture", new XAttribute("texture", "Content/Lights/light.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 RemoveAttack() + { + if (RemoveSubParam(Attack)) + { + Attack = null; + return true; + } + return false; + } + + public bool RemoveSound() + { + if (RemoveSubParam(Sound)) + { + Sound = null; + return true; + } + return false; + } + + public bool RemoveLight() + { + if (RemoveSubParam(LightSource)) + { + LightSource = null; + return true; + } + return false; + } + + 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 + { + subParam = constructor(element, Ragdoll); + if (collection != null && filter != null) + { + if (filter(collection)) { return false; } + } + Element.Add(element); + SubParams.Add(subParam); + collection?.Add(subParam); + return subParam != null; + } + + protected bool RemoveSubParam(T subParam, IList collection = null) where T : SubParam + { + if (subParam == null || subParam.Element == null || subParam.Element.Parent == null) { return false; } + if (collection != null && !collection.Contains(subParam)) { return false; } + if (!SubParams.Contains(subParam)) { return false; } + collection?.Remove(subParam); + SubParams.Remove(subParam); + subParam.Element.Remove(); + return true; + } + } + + public class DecorativeSpriteParams : SpriteParams + { + public DecorativeSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + { +#if CLIENT + DecorativeSprite = new DecorativeSprite(element); +#endif + } + +#if CLIENT + public DecorativeSprite DecorativeSprite { get; private set; } + + public override bool Deserialize(XElement element = null, bool recursive = true) + { + base.Deserialize(element, recursive); + DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, element ?? Element); + return SerializableProperties != null; + } + + public override bool Serialize(XElement element = null, bool recursive = true) + { + base.Serialize(element, recursive); + SerializableProperty.SerializeProperties(DecorativeSprite, element ?? Element); + return true; + } + + public override void Reset() + { + base.Reset(); + DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, OriginalElement); + } +#endif + } + + public class DeformSpriteParams : SpriteParams + { + public DeformationParams Deformation { get; private set; } + + public DeformSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + { + Deformation = new DeformationParams(element, ragdoll); + SubParams.Add(Deformation); + } + } + + public class SpriteParams : SubParam + { + [Serialize("0, 0, 0, 0", true), 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)] + 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)] + public float Depth { get; set; } + + [Serialize("", true), Editable()] + public string Texture { get; set; } + + public override string Name => "Sprite"; + + public SpriteParams(XElement 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) + { +#if CLIENT + Deformations = new Dictionary(); + foreach (var deformationElement in element.GetChildElements("spritedeformation")) + { + string typeName = deformationElement.GetAttributeString("typename", null) ?? deformationElement.GetAttributeString("type", ""); + SpriteDeformationParams deformation = null; + switch (typeName.ToLowerInvariant()) + { + case "inflate": + deformation = new InflateParams(deformationElement); + break; + case "custom": + deformation = new CustomDeformationParams(deformationElement); + break; + case "noise": + deformation = new NoiseDeformationParams(deformationElement); + break; + case "jointbend": + case "bendjoint": + deformation = new JointBendDeformationParams(deformationElement); + break; + case "reacttotriggerers": + deformation = new PositionalDeformationParams(deformationElement); + break; + default: + DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'"); + break; + } + if (deformation != null) + { + deformation.TypeName = typeName; + } + Deformations.Add(deformation, deformationElement); + } +#endif + } + +#if CLIENT + public Dictionary Deformations { get; private set; } + + public override bool Deserialize(XElement element = null, bool recursive = true) + { + base.Deserialize(element, recursive); + Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value)); + return SerializableProperties != null; + } + + public override bool Serialize(XElement element = null, bool recursive = true) + { + base.Serialize(element, recursive); + Deformations.ForEach(d => SerializableProperty.SerializeProperties(d.Key, d.Value)); + return true; + } + + public override void Reset() + { + base.Reset(); + Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value)); + } +#endif + } + + public class ColliderParams : SubParam + { + private string name; + [Serialize("", true), Editable] + public override string Name + { + get + { + if (string.IsNullOrWhiteSpace(name)) + { + name = GenerateName(); + } + return name; + } + set + { + name = value; + } + } + + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + public float Radius { get; set; } + + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + public float Height { get; set; } + + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + public float Width { get; set; } + + public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) + { + Name = name; + } + } + + public class LightSourceParams : SubParam + { + public class LightTexture : SubParam + { + public override string Name => "Light Texture"; + + [Serialize("", true), Editable] + public string Texture { get; private set; } + + [Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)] + public Vector2 Origin { get; set; } + + [Serialize("1.0, 1.0", true), Editable(DecimalCount = 2)] + public Vector2 Size { get; set; } + + public LightTexture(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + } + + public LightTexture Texture { get; private set; } + +#if CLIENT + public Lights.LightSourceParams LightSource { get; private set; } +#endif + + public LightSourceParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + { +#if CLIENT + LightSource = new Lights.LightSourceParams(element); +#endif + var lightTextureElement = element.GetChildElement("lighttexture"); + if (lightTextureElement != null) + { + Texture = new LightTexture(lightTextureElement, ragdoll); + SubParams.Add(Texture); + } + } + +#if CLIENT + public override bool Deserialize(XElement element = null, bool recursive = true) + { + base.Deserialize(element, recursive); + LightSource.Deserialize(element ?? Element); + return SerializableProperties != null; + } + + public override bool Serialize(XElement element = null, bool recursive = true) + { + base.Serialize(element, recursive); + LightSource.Serialize(element ?? Element); + return true; + } + + public override void Reset() + { + base.Reset(); + LightSource.Serialize(OriginalElement); + } +#endif + } + + // TODO: conditionals? + public class AttackParams : SubParam + { + public Attack Attack { get; private set; } + + public AttackParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + { + Attack = new Attack(element, ragdoll.SpeciesName); + } + + public override bool Deserialize(XElement element = null, bool recursive = true) + { + base.Deserialize(element, recursive); + Attack.Deserialize(element ?? Element); + return SerializableProperties != null; + } + + public override bool Serialize(XElement element = null, bool recursive = true) + { + base.Serialize(element, recursive); + Attack.Serialize(element ?? Element); + return true; + } + + public override void Reset() + { + base.Reset(); + Attack.Deserialize(OriginalElement); + Attack.ReloadAfflictions(OriginalElement); + } + + public bool AddNewAffliction() + { + Serialize(); + var subElement = new XElement("affliction", + new XAttribute("identifier", "internaldamage"), + new XAttribute("strength", 0f), + new XAttribute("probability", 1.0f)); + Element.Add(subElement); + Attack.ReloadAfflictions(Element); + Serialize(); + return true; + } + + public bool RemoveAffliction(XElement affliction) + { + Serialize(); + affliction.Remove(); + Attack.ReloadAfflictions(Element); + return Serialize(); + } + } + + public class DamageModifierParams : SubParam + { + public DamageModifier DamageModifier { get; private set; } + + public DamageModifierParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + { + DamageModifier = new DamageModifier(element, ragdoll.SpeciesName); + } + + public override bool Deserialize(XElement element = null, bool recursive = true) + { + base.Deserialize(element, recursive); + DamageModifier.Deserialize(element ?? Element); + return SerializableProperties != null; + } + + public override bool Serialize(XElement element = null, bool recursive = true) + { + base.Serialize(element, recursive); + DamageModifier.Serialize(element ?? Element); + return true; + } + + public override void Reset() + { + base.Reset(); + DamageModifier.Deserialize(OriginalElement); + } + } + + public class SoundParams : SubParam + { + public override string Name => "Sound"; + + [Serialize("", true), Editable] + public string Tag { get; private set; } + + public SoundParams(XElement 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 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) + { + Element = element; + OriginalElement = new XElement(element); + Ragdoll = ragdoll; + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public virtual bool Deserialize(XElement element = null, bool recursive = true) + { + element = element ?? Element; + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + if (recursive) + { + SubParams.ForEach(sp => sp.Deserialize(recursive: true)); + } + return SerializableProperties != null; + } + + public virtual bool Serialize(XElement element = null, bool recursive = true) + { + element = element ?? Element; + SerializableProperty.SerializeProperties(this, element, true); + if (recursive) + { + SubParams.ForEach(sp => sp.Serialize(recursive: true)); + } + return true; + } + + public virtual void SetCurrentElementAsOriginalElement() + { + OriginalElement = Element; + SubParams.ForEach(sp => sp.SetCurrentElementAsOriginalElement()); + } + + public virtual void Reset() + { + // Don't use recursion, because the reset method might be overriden + Deserialize(OriginalElement, false); + SubParams.ForEach(sp => sp.Reset()); + } + +#if CLIENT + public SerializableEntityEditor SerializableEntityEditor { get; protected set; } + 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); + if (this is DecorativeSpriteParams decSpriteParams) + { + new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUI.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); + } + } + else if (this is AttackParams attackParams) + { + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUI.LargeFont); + if (AfflictionEditors == null) + { + AfflictionEditors = new Dictionary(); + } + else + { + AfflictionEditors.Clear(); + } + foreach (var affliction in attackParams.Attack.Afflictions.Keys) + { + var afflictionEditor = new SerializableEntityEditor(SerializableEntityEditor.RectTransform, affliction, inGame: false, showName: true); + AfflictionEditors.Add(affliction, afflictionEditor); + SerializableEntityEditor.AddCustomContent(afflictionEditor, SerializableEntityEditor.ContentCount); + } + } + else if (this is LightSourceParams lightParams) + { + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUI.LargeFont); + } + else if (this is DamageModifierParams damageModifierParams) + { + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUI.LargeFont); + } + if (recursive) + { + SubParams.ForEach(sp => sp.AddToEditor(editor, true)); + } + if (space > 0) + { + new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: new Color(20, 20, 20, 255)) + { + CanBeFocused = false + }; + } + } +#endif + } + #endregion + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Source/ContentPackage.cs b/Barotrauma/BarotraumaShared/Source/ContentPackage.cs index 414e8d982..e6347551c 100644 --- a/Barotrauma/BarotraumaShared/Source/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/Source/ContentPackage.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -34,10 +35,11 @@ namespace Barotrauma Decals, NPCConversations, Afflictions, - Buffs, Tutorials, UIStyle, - TraitorMissions + TraitorMissions, + EventManagerSettings, + Orders } public class ContentPackage @@ -61,7 +63,8 @@ namespace Barotrauma ContentType.LevelObjectPrefabs, ContentType.RuinConfig, ContentType.Outpost, - ContentType.Afflictions + ContentType.Afflictions, + ContentType.Orders }; //at least one file of each these types is required in core content packages @@ -80,12 +83,11 @@ namespace Barotrauma ContentType.LevelGenerationParameters, ContentType.RandomEvents, ContentType.Missions, - ContentType.TraitorMissions, - ContentType.BackgroundCreaturePrefabs, ContentType.RuinConfig, - ContentType.NPCConversations, ContentType.Afflictions, - ContentType.UIStyle + ContentType.UIStyle, + ContentType.EventManagerSettings, + ContentType.Orders }; public static IEnumerable CorePackageRequiredFiles @@ -175,7 +177,7 @@ namespace Barotrauma 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; + type = ContentType.None; } Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type)); } @@ -191,6 +193,29 @@ namespace Barotrauma } } + private bool? invalid; + public bool Invalid + { + get + { + if (!invalid.HasValue) + { + invalid = !CheckValidity(out _); + } + return invalid.Value; + } + } + + private List errorMessages; + public IEnumerable ErrorMessages + { + get + { + if (errorMessages == null) { CheckValidity(out _); } + return errorMessages; + } + } + public override string ToString() { return Name; @@ -233,6 +258,58 @@ namespace Barotrauma return missingContentTypes.Count == 0; } + public bool CheckValidity(out List errorMessages) + { + this.errorMessages = errorMessages = new List(); + foreach (ContentFile file in Files) + { + switch (file.Type) + { + case ContentType.Executable: + case ContentType.ServerExecutable: + case ContentType.None: + case ContentType.Outpost: + case ContentType.Submarine: + break; + default: + try + { + XDocument.Load(file.Path); + } + 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 e; +#endif + } + } + break; + } + } + + if (CorePackage && !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); + invalid = errorMessages.Count > 0; + return !invalid.Value; + } + /// /// Make sure all the files defined in the content package are present /// @@ -246,7 +323,6 @@ namespace Barotrauma //dedicated server doesn't care if the client executable is present or not if (file.Type == ContentType.Executable) { continue; } #endif - if (!File.Exists(file.Path)) { errorMessages.Add("File \"" + file.Path + "\" not found."); @@ -368,12 +444,18 @@ namespace Barotrauma { case ContentType.Character: XDocument doc = XMLExtensions.TryLoadXml(file.Path); - string speciesName = doc.Root.GetAttributeString("name", ""); - //TODO: check non-default paths if defined - filePaths.Add(RagdollParams.GetDefaultFile(speciesName, this)); - foreach (AnimationType animationType in Enum.GetValues(typeof(AnimationType))) + var rootElement = doc.Root; + var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; + var speciesName = element.GetAttributeString("speciesname", element.GetAttributeString("name", "")); + var ragdollFolder = RagdollParams.GetFolder(speciesName); + if (Directory.Exists(ragdollFolder)) { - filePaths.Add(AnimationParams.GetDefaultFile(speciesName, animationType, this)); + Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f)); + } + var animationFolder = AnimationParams.GetFolder(speciesName); + if (Directory.Exists(animationFolder)) + { + Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f)); } break; } @@ -445,7 +527,7 @@ namespace Barotrauma } /// - /// Returns all xml files. + /// Returns all xml files from all the loaded content packages. /// public static IEnumerable GetAllContentFiles(IEnumerable contentPackages) { @@ -478,12 +560,13 @@ namespace Barotrauma } } + string[] files = Directory.GetFiles(folder, "*.xml"); + List.Clear(); - string[] files = Directory.GetFiles(folder, "*.xml"); foreach (string filePath in files) { - List.Add(new ContentPackage(filePath)); + List.Add(new ContentPackage(filePath)); } string[] modDirectories = Directory.GetDirectories("Mods"); @@ -498,12 +581,27 @@ namespace Barotrauma } } + public static void SortContentPackages() + { + List = List + .OrderByDescending(p => p.CorePackage) + .ThenByDescending(p => GameMain.Config?.SelectedContentPackages.Contains(p)) + .ThenBy(p => GameMain.Config?.SelectedContentPackages.IndexOf(p)) + .ToList(); + + if (GameMain.Config != null) + { + var reportList = List.Where(p => GameMain.Config.SelectedContentPackages.Contains(p)); + DebugConsole.NewMessage($"Content package load order: { new string(reportList.SelectMany(cp => cp.Name + " | ").ToArray()) }"); + } + } + public void Delete() { try { File.Delete(Path); - GameMain.Config.SelectedContentPackages.Remove(this); + GameMain.Config.DeselectContentPackage(this); GameMain.Config.SaveNewPlayerConfig(); } catch (IOException e) @@ -512,6 +610,7 @@ namespace Barotrauma return; } List.Remove(this); + SortContentPackages(); } } diff --git a/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs b/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs index b9af5f57a..4ea22f845 100644 --- a/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs @@ -195,6 +195,9 @@ namespace Barotrauma } catch (Exception e) { +#if CLIENT && WINDOWS + if (e is SharpDX.SharpDXException) { throw; } +#endif DebugConsole.ThrowError("Coroutine " + handle.Name + " threw an exception: " + e.Message + "\n" + e.StackTrace.ToString()); handle.Exception = e; return true; diff --git a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs index aa41e2699..9f4b54c52 100644 --- a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs @@ -209,7 +209,7 @@ namespace Barotrauma characterFiles[i] = Path.GetFileNameWithoutExtension(characterFiles[i]).ToLowerInvariant(); } - foreach (JobPrefab jobPrefab in JobPrefab.List) + foreach (JobPrefab jobPrefab in JobPrefab.List.Values) { characterFiles.Add(jobPrefab.Name); } @@ -618,7 +618,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() }; }, isCheat: true)); @@ -626,6 +626,9 @@ namespace Barotrauma { Character.Controlled = null; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; +#if CLIENT + GameMain.Client?.SendConsoleCommand("freecam"); +#endif }, isCheat: true)); commands.Add(new Command("eventmanager", "eventmanager: Toggle event manager on/off. No new random events are created when the event manager is disabled.", (string[] args) => @@ -965,7 +968,6 @@ namespace Barotrauma })); #if DEBUG - /*TODO: reimplement commands.Add(new Command("simulatedlatency", "simulatedlatency [minimumlatencyseconds] [randomlatencyseconds]: applies a simulated latency to network messages. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) => { if (args.Count() < 2 || (GameMain.NetworkMember == null)) return; @@ -982,18 +984,19 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { - GameMain.Client.NetPeerConfiguration.SimulatedMinimumLatency = minimumLatency; - GameMain.Client.NetPeerConfiguration.SimulatedRandomLatency = randomLatency; + GameMain.Client.SimulatedMinimumLatency = minimumLatency; + GameMain.Client.SimulatedRandomLatency = randomLatency; } #elif SERVER if (GameMain.Server != null) { - GameMain.Server.NetPeerConfiguration.SimulatedMinimumLatency = minimumLatency; - GameMain.Server.NetPeerConfiguration.SimulatedRandomLatency = randomLatency; + GameMain.Server.SimulatedMinimumLatency = minimumLatency; + GameMain.Server.SimulatedRandomLatency = randomLatency; } #endif NewMessage("Set simulated minimum latency to " + minimumLatency + " and random latency to " + randomLatency + ".", Color.White); })); + commands.Add(new Command("simulatedloss", "simulatedloss [lossratio]: applies simulated packet loss to network messages. For example, a value of 0.1 would mean 10% of the packets are dropped. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) => { if (args.Count() < 1 || (GameMain.NetworkMember == null)) return; @@ -1005,12 +1008,12 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { - GameMain.Client.NetPeerConfiguration.SimulatedLoss = loss; + GameMain.Client.SimulatedLoss = loss; } #elif SERVER if (GameMain.Server != null) { - GameMain.Server.NetPeerConfiguration.SimulatedLoss = loss; + GameMain.Server.SimulatedLoss = loss; } #endif NewMessage("Set simulated packet loss to " + (int)(loss * 100) + "%.", Color.White); @@ -1026,16 +1029,16 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { - GameMain.Client.NetPeerConfiguration.SimulatedDuplicatesChance = duplicates; + GameMain.Client.SimulatedDuplicatesChance = duplicates; } #elif SERVER if (GameMain.Server != null) { - GameMain.Server.NetPeerConfiguration.SimulatedDuplicatesChance = duplicates; + GameMain.Server.SimulatedDuplicatesChance = duplicates; } #endif NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White); - }));*/ + })); #endif //"dummy commands" that only exist so that the server can give clients permissions to use them @@ -1338,9 +1341,12 @@ namespace Barotrauma WayPoint spawnPoint = null; string characterLowerCase = args[0].ToLowerInvariant(); - JobPrefab job = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == characterLowerCase || jp.Identifier.ToLowerInvariant() == characterLowerCase); - bool human = job != null || characterLowerCase == "human"; - + if (!JobPrefab.List.TryGetValue(characterLowerCase, out JobPrefab job)) + { + job = JobPrefab.List.Values.FirstOrDefault(jp => jp.Name?.ToLowerInvariant() == characterLowerCase); + } + bool human = job != null || characterLowerCase == Character.HumanSpeciesName; + if (args.Length > 1) { switch (args[1].ToLowerInvariant()) @@ -1389,7 +1395,7 @@ namespace Barotrauma if (human) { - CharacterInfo characterInfo = new CharacterInfo(Character.HumanConfigFile, jobPrefab: job); + CharacterInfo characterInfo = new CharacterInfo(Character.HumanSpeciesName, jobPrefab: job); spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8)); if (job != null) { @@ -1410,23 +1416,10 @@ namespace Barotrauma } else { - IEnumerable characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character); - foreach (string characterFile in characterFiles) + if (Character.GetConfigFilePath(args[0]) != null) { - if (Path.GetFileNameWithoutExtension(characterFile).ToLowerInvariant() == args[0].ToLowerInvariant()) - { - Character.Create(characterFile, spawnPosition, ToolBox.RandomSeed(8)); - return; - } + Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8)); } - - errorMsg = "No character matching the name \"" + args[0] + "\" found in the selected content package."; - - //attempt to open the config from the default path (the file may still be present even if it isn't included in the content package) - string configPath = "Content/Characters/" - + args[0].First().ToString().ToUpper() + args[0].Substring(1) - + "/" + args[0].ToLower() + ".xml"; - Character.Create(configPath, spawnPosition, ToolBox.RandomSeed(8)); } } @@ -1437,7 +1430,23 @@ namespace Barotrauma Vector2? spawnPos = null; Inventory spawnInventory = null; - + + string itemName = args[0].ToLowerInvariant(); + if (!(MapEntityPrefab.Find(itemName, showErrorMessages: false) is ItemPrefab itemPrefab)) + { + errorMsg = "Item \"" + itemName + "\" not found!"; + var matching = MapEntityPrefab.List.Find(me => me.Name.ToLowerInvariant().StartsWith(itemName) && me is ItemPrefab); + if (matching != null) + { + errorMsg += $" Did you mean \"{matching.Name}\"?"; + if (matching.Name.Contains(" ")) + { + errorMsg += $" Please note that you should surround multi-word names with quotation marks (e.q. spawnitem \"{matching.Name}\")"; + } + } + return; + } + if (args.Length > 1) { switch (args.Last()) @@ -1461,14 +1470,7 @@ namespace Barotrauma break; } } - - string itemName = args[0].ToLowerInvariant(); - if (!(MapEntityPrefab.Find(itemName) is ItemPrefab itemPrefab)) - { - errorMsg = "Item \"" + itemName + "\" not found!"; - return; - } - + if ((spawnPos == null || spawnPos == Vector2.Zero) && spawnInventory == null) { var wp = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); @@ -1550,30 +1552,36 @@ namespace Barotrauma } else { - int parsedNum = 0; - if (!int.TryParse(currNum, out parsedNum)) + if (!int.TryParse(currNum, out int parsedNum) || parsedNum < 0) { return false; } - - switch (c) + try { - case 'd': - timeSpan += new TimeSpan(parsedNum, 0, 0, 0, 0); - break; - case 'h': - timeSpan += new TimeSpan(0, parsedNum, 0, 0, 0); - break; - case 'm': - timeSpan += new TimeSpan(0, 0, parsedNum, 0, 0); - break; - case 's': - timeSpan += new TimeSpan(0, 0, 0, parsedNum, 0); - break; - default: - return false; + switch (c) + { + case 'd': + timeSpan += new TimeSpan(parsedNum, 0, 0, 0, 0); + break; + case 'h': + timeSpan += new TimeSpan(0, parsedNum, 0, 0, 0); + break; + case 'm': + timeSpan += new TimeSpan(0, 0, parsedNum, 0, 0); + break; + case 's': + timeSpan += new TimeSpan(0, 0, 0, parsedNum, 0); + break; + default: + return false; + } + } + catch (ArgumentOutOfRangeException) + { + ThrowError($"{parsedNum} {c} exceeds the maximum supported time span. Using the maximum time span {TimeSpan.MaxValue} instead."); + timeSpan = TimeSpan.MaxValue; + return true; } - currNum = ""; } } @@ -1598,13 +1606,17 @@ namespace Barotrauma if (e != null) { error += " {" + e.Message + "}\n" + e.StackTrace; + if (e.InnerException != null) + { + error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace; + } } System.Diagnostics.Debug.WriteLine(error); NewMessage(error, Color.Red); #if CLIENT if (createMessageBox) { - new GUIMessageBox(TextManager.Get("Error"), error); + CoroutineManager.StartCoroutine(CreateMessageBox(error)); } else { @@ -1612,7 +1624,20 @@ namespace Barotrauma } #endif } - + +#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; + } +#endif + public static void SaveLogs() { if (unsavedMessages.Count == 0) return; diff --git a/Barotrauma/BarotraumaShared/Source/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/Source/Events/ArtifactEvent.cs index f31d940fd..0bda67c0c 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/ArtifactEvent.cs @@ -71,7 +71,7 @@ namespace Barotrauma var itemContainer = it.GetComponent(); if (itemContainer == null) continue; - if (itemContainer.Combine(item)) break; // Placement successful + if (itemContainer.Combine(item, user: null)) break; // Placement successful } if (GameSettings.VerboseLogging) diff --git a/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs b/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs index cb5365874..e7009bf6a 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs @@ -8,7 +8,7 @@ namespace Barotrauma { const float IntensityUpdateInterval = 5.0f; - private List events; + private readonly List events; private Level level; @@ -33,7 +33,7 @@ namespace Barotrauma private float roundDuration; - private List selectedEventSets; + private readonly List selectedEventSets; private EventManagerSettings settings; @@ -168,14 +168,17 @@ namespace Barotrauma if (eventSet.EventPrefabs.Count > 0) { MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); - var newEvent = eventSet.EventPrefabs[rand.NextInt32() % eventSet.EventPrefabs.Count].CreateInstance(); - newEvent.Init(true); - DebugConsole.Log("Initialized event " + newEvent.ToString()); - events.Add(newEvent); + var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand); + if (eventPrefab != null) + { + var newEvent = eventPrefab.CreateInstance(); + newEvent.Init(true); + DebugConsole.Log("Initialized event " + newEvent.ToString()); + events.Add(newEvent); + } } if (eventSet.ChildSets.Count > 0) { - MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); var newEventSet = SelectRandomEvents(eventSet.ChildSets); if (newEventSet != null) selectedEventSets.Add(newEventSet); } diff --git a/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs b/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs index 40121b408..317d69926 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.Collections.Generic; using System.Xml.Linq; +using System.Linq; +using System; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -28,17 +28,42 @@ namespace Barotrauma static EventManagerSettings() { - Load(Path.Combine("Content", "EventManagerSettings.xml")); + foreach (string file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings)) + { + Load(file); + } } private static void Load(string file) { XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null || doc.Root == null) return; - - foreach (XElement subElement in doc.Root.Elements()) + if (doc == null) { return; } + var mainElement = doc.Root; + bool allowOverriding = false; + if (doc.Root.IsOverride()) { - List.Add(new EventManagerSettings(subElement)); + mainElement = doc.Root.FirstElement(); + allowOverriding = true; + } + foreach (XElement subElement in mainElement.Elements()) + { + var element = subElement.IsOverride() ? subElement.FirstElement() : subElement; + string name = element.Name.ToString(); + var duplicate = List.FirstOrDefault(e => e.Name.ToString().Equals(name, StringComparison.OrdinalIgnoreCase)); + if (duplicate != null) + { + if (allowOverriding || subElement.IsOverride()) + { + DebugConsole.NewMessage($"Overriding the existing preset '{name}' in the event manager settings using the file '{file}'", Color.Yellow); + List.Remove(duplicate); + } + else + { + DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{name}' found! Each element must have a unique name. Use tags if you want to override an existing preset."); + continue; + } + } + List.Add(new EventManagerSettings(element)); } } diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs index bada49ee8..4059a95c6 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs @@ -94,7 +94,7 @@ namespace Barotrauma items.Add(item); - if (parent != null) parent.Combine(item); + if (parent != null) parent.Combine(item, user: null); foreach (XElement subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs index b0d878033..51ecaa0b7 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs @@ -32,7 +32,7 @@ namespace Barotrauma if (Winner == Character.TeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; } //disable success message for now if it hasn't been translated - if (!TextManager.ContainsTag("MissionSuccess." + Prefab.Identifier)) { return ""; } + if (!TextManager.ContainsTag("MissionSuccess." + Prefab.TextIdentifier)) { return ""; } var loser = Winner == Character.TeamType.Team1 ? Character.TeamType.Team2 : @@ -49,9 +49,9 @@ namespace Barotrauma { descriptions = new string[] { - TextManager.Get("MissionDescriptionNeutral." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("descriptionneutral", ""), - TextManager.Get("MissionDescription1." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("description1", ""), - TextManager.Get("MissionDescription2." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("description2", "") + 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", "") }; for (int i = 0; i < descriptions.Length; i++) @@ -64,8 +64,8 @@ namespace Barotrauma teamNames = new string[] { - TextManager.Get("MissionTeam1." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname1", "Team A"), - TextManager.Get("MissionTeam2." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname2", "Team B") + TextManager.Get("MissionTeam1." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname1", "Team A"), + TextManager.Get("MissionTeam2." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname2", "Team B") }; } diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs index 486e84442..31bcae2e1 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using System.Xml.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -15,7 +16,7 @@ namespace Barotrauma Combat } - class MissionPrefab + partial class MissionPrefab { public static readonly List List = new List(); @@ -27,7 +28,7 @@ namespace Barotrauma { MissionType.Combat, typeof(CombatMission) }, }; - private ConstructorInfo constructor; + private readonly ConstructorInfo constructor; public readonly MissionType type; @@ -35,6 +36,8 @@ namespace Barotrauma public readonly string Identifier; + public readonly string TextIdentifier; + public readonly string Name; public readonly string Description; public readonly string SuccessMessage; @@ -61,10 +64,34 @@ namespace Barotrauma foreach (string file in files) { XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc?.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) + 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}'", Color.Yellow); + List.Remove(duplicate); + } + else + { + DebugConsole.ThrowError($"Duplicate mission found with the identifier '{identifier}' in file '{file}'! 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)); } } @@ -75,15 +102,16 @@ namespace Barotrauma ConfigElement = element; Identifier = element.GetAttributeString("identifier", ""); + TextIdentifier = element.GetAttributeString("textidentifier", null) ?? Identifier; - Name = TextManager.Get("MissionName." + Identifier, true) ?? element.GetAttributeString("name", ""); - Description = TextManager.Get("MissionDescription." + Identifier, true) ?? element.GetAttributeString("description", ""); + Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", ""); + Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", ""); Reward = element.GetAttributeInt("reward", 1); Commonness = element.GetAttributeInt("commonness", 1); - SuccessMessage = TextManager.Get("MissionSuccess." + Identifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully"); - FailureMessage = TextManager.Get("MissionFailure." + Identifier, true) ?? ""; + 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) ?? ""; @@ -93,7 +121,7 @@ namespace Barotrauma FailureMessage = element.GetAttributeString("failuremessage", ""); } - SonarLabel = TextManager.Get("MissionSonarLabel." + Identifier, true) ?? element.GetAttributeString("sonarlabel", ""); + SonarLabel = TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? element.GetAttributeString("sonarlabel", ""); MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false); @@ -110,8 +138,8 @@ namespace Barotrauma case "message": int index = Messages.Count; - Headers.Add(TextManager.Get("MissionHeader" + index + "." + Identifier, true) ?? subElement.GetAttributeString("header", "")); - Messages.Add(TextManager.Get("MissionMessage" + index + "." + Identifier, true) ?? subElement.GetAttributeString("text", "")); + Headers.Add(TextManager.Get("MissionHeader" + index + "." + TextIdentifier, true) ?? subElement.GetAttributeString("header", "")); + Messages.Add(TextManager.Get("MissionMessage" + index + "." + TextIdentifier, true) ?? subElement.GetAttributeString("text", "")); break; case "locationtype": AllowedLocationTypes.Add(new Pair( @@ -139,7 +167,11 @@ namespace Barotrauma } constructor = missionClasses[type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + + InitProjSpecific(element); } + + partial void InitProjSpecific(XElement element); public bool IsAllowed(Location from, Location to) { diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs index f0728eb71..0f8e2459d 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs @@ -80,7 +80,7 @@ namespace Barotrauma var itemContainer = it.GetComponent(); if (itemContainer == null) continue; - if (itemContainer.Combine(item)) break; // Placement successful + if (itemContainer.Combine(item, user: null)) break; // Placement successful } } } diff --git a/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventPrefab.cs b/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventPrefab.cs index dc4dac97a..60e022532 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventPrefab.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Xml.Linq; @@ -12,6 +13,8 @@ namespace Barotrauma public readonly string MusicType; + public float Commonness; + public ScriptedEventPrefab(XElement element) { ConfigElement = element; @@ -30,6 +33,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); } + Commonness = element.GetAttributeFloat("commonness", 1.0f); } public ScriptedEvent CreateInstance() diff --git a/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventSet.cs b/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventSet.cs index 20df02eb9..c59bd6f88 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventSet.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/ScriptedEventSet.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Xml.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -85,8 +85,9 @@ namespace Barotrauma public float GetCommonness(Level level) { - return Commonness.ContainsKey(level.GenerationParams.Name) ? - Commonness[level.GenerationParams.Name] : Commonness[""]; + string key = level.GenerationParams?.Name ?? ""; + return Commonness.ContainsKey(key) ? + Commonness[key] : Commonness[""]; } public static void LoadPrefabs() @@ -103,12 +104,19 @@ namespace Barotrauma foreach (string configFile in configFiles) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null) continue; + 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}", Color.Yellow); + List.Clear(); + } int i = 0; foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != "eventset") continue; + if (element.Name.ToString().ToLowerInvariant() != "eventset") { continue; } List.Add(new ScriptedEventSet(element, i.ToString())); i++; } diff --git a/Barotrauma/BarotraumaShared/Source/Extensions/RectangleExtensions.cs b/Barotrauma/BarotraumaShared/Source/Extensions/RectangleExtensions.cs index 78365fbba..b76860792 100644 --- a/Barotrauma/BarotraumaShared/Source/Extensions/RectangleExtensions.cs +++ b/Barotrauma/BarotraumaShared/Source/Extensions/RectangleExtensions.cs @@ -4,6 +4,18 @@ namespace Barotrauma.Extensions { public static class RectangleExtensions { + public static Rectangle Multiply(this Rectangle rect, float f) + { + Vector2 location = new Vector2(rect.X, rect.Y) * f; + return new Rectangle(new Point((int)location.X, (int)location.Y), rect.MultiplySize(f)); + } + + public static Rectangle Divide(this Rectangle rect, float f) + { + Vector2 location = new Vector2(rect.X, rect.Y) / f; + return new Rectangle(new Point((int)location.X, (int)location.Y), rect.DivideSize(f)); + } + public static Point DivideSize(this Rectangle rect, float f) { return new Point((int)(rect.Width / f), (int)(rect.Height / f)); diff --git a/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs b/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs index 9aa6cdada..f13e5a0de 100644 --- a/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs +++ b/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs @@ -23,6 +23,11 @@ namespace Barotrauma } return new string(newString.SelectMany(str => str.ToCharArray()).ToArray()); } + + public static string Remove(this string s, string substring) + { + return s.Replace(substring, string.Empty); + } public static string Remove(this string s, Func predicate) { diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs index da06dc538..6e72d7b1a 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs @@ -13,7 +13,7 @@ namespace Barotrauma public bool CheatsEnabled; const int InitialMoney = 8700; - public const int HullRepairCost = 500, ItemRepairCost = 500; + public const int HullRepairCost = 500, ItemRepairCost = 500, ShuttleReplaceCost = 1000; protected bool watchmenSpawned; protected Character startWatchman, endWatchman; @@ -21,7 +21,7 @@ namespace Barotrauma //key = dialog flag, double = Timing.TotalTime when the line was last said private Dictionary dialogLastSpoken = new Dictionary(); - public bool PurchasedHullRepairs, PurchasedItemRepairs; + public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs; protected Map map; public Map Map @@ -83,7 +83,7 @@ namespace Barotrauma { for (int i = 0; i < wall.SectionCount; i++) { - wall.AddDamage(i, -100000.0f); + wall.AddDamage(i, -wall.Prefab.Health); } } } @@ -104,6 +104,7 @@ namespace Barotrauma } PurchasedItemRepairs = false; } + PurchasedLostShuttles = false; } public override void Update(float deltaTime) @@ -169,8 +170,8 @@ namespace Barotrauma string seed = outpost == Level.Loaded.StartOutpost ? map.SelectedLocation.Name : map.CurrentLocation.Name; Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); - JobPrefab watchmanJob = JobPrefab.List.Find(jp => jp.Identifier == "watchman"); - CharacterInfo characterInfo = new CharacterInfo(Character.HumanConfigFile, jobPrefab: watchmanJob); + JobPrefab watchmanJob = JobPrefab.Get("watchman"); + CharacterInfo characterInfo = new CharacterInfo(Character.HumanSpeciesName, jobPrefab: watchmanJob); var spawnedCharacter = Character.Create(characterInfo, watchmanSpawnpoint.WorldPosition, Level.Loaded.Seed + (outpost == Level.Loaded.StartOutpost ? "start" : "end")); InitializeWatchman(spawnedCharacter); diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameModePreset.cs index 048cfb896..bbe5dd8e1 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameModePreset.cs @@ -7,48 +7,23 @@ namespace Barotrauma class GameModePreset { public static List List = new List(); - - public ConstructorInfo Constructor - { - get; - private set; - } - public string Name - { - get; - private set; - } + public readonly ConstructorInfo Constructor; - public string Identifier - { - get; - private set; - } + public readonly string Name; + public readonly string Description; - public bool IsSinglePlayer - { - get; - private set; - } + public readonly string Identifier; + + public readonly bool IsSinglePlayer; //are clients allowed to vote for this gamemode - public bool Votable - { - get; - private set; - } - - //TODO: translate mission descriptions - public string Description - { - get; - private set; - } + public readonly bool Votable; public GameModePreset(string identifier, Type type, bool isSinglePlayer = false, bool votable = true) { Name = TextManager.Get("GameMode." + identifier); + Description = TextManager.Get("GameModeDescription." + identifier, returnNull: true) ?? ""; Identifier = identifier; Constructor = type.GetConstructor(new Type[] { typeof(GameModePreset), typeof(object) }); @@ -70,23 +45,10 @@ namespace Barotrauma #if CLIENT new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true); new GameModePreset("tutorial", typeof(TutorialMode), true); - new GameModePreset("devsandbox", typeof(GameMode), true) - { - Description = "Single player sandbox mode for debugging." - }; + new GameModePreset("devsandbox", typeof(GameMode), true); #endif - new GameModePreset("sandbox", typeof(GameMode), false) - { - Description = "A game mode with no specific objectives." - }; - - new GameModePreset("mission", typeof(MissionMode), false) - { - Description = "The crew must work together to complete a specific task, such as retrieving " - + "an alien artifact or killing a creature that's terrorizing nearby outposts. The game ends " - + "when the task is completed or everyone in the crew has died." - }; - + new GameModePreset("sandbox", typeof(GameMode), false); + new GameModePreset("mission", typeof(MissionMode), false); new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false); } } diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs index 2a478e78f..d090dd4c1 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs @@ -166,11 +166,11 @@ namespace Barotrauma if (Submarine == null) { - DebugConsole.ThrowError("Couldn't start game session, submarine not selected"); + DebugConsole.ThrowError("Couldn't start game session, submarine not selected."); return; } - if (reloadSub || Submarine.MainSub != Submarine) Submarine.Load(true); + if (reloadSub || Submarine.MainSub != Submarine) { Submarine.Load(true); } Submarine.MainSub = Submarine; if (loadSecondSub) { @@ -184,6 +184,12 @@ namespace Barotrauma Submarine.MainSubs[1].Load(false); } } + + if (Submarine.IsFileCorrupted) + { + DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted."); + return; + } if (level != null) { diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/Source/GameSession/HireManager.cs index a706f1cc2..a08253c95 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/HireManager.cs @@ -31,7 +31,7 @@ namespace Barotrauma JobPrefab job = location.Type.GetRandomHireable(); if (job == null) { return; } - availableCharacters.Add(new CharacterInfo(Character.HumanConfigFile, "", job)); + availableCharacters.Add(new CharacterInfo(Character.HumanSpeciesName, "", job)); } } diff --git a/Barotrauma/BarotraumaShared/Source/GameSettings.cs b/Barotrauma/BarotraumaShared/Source/GameSettings.cs index b036c1ed7..8a1cc5411 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSettings.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using System.Xml; using System.IO; +using Barotrauma.Extensions; #if CLIENT using Microsoft.Xna.Framework.Graphics; using Barotrauma.Tutorials; @@ -205,7 +206,7 @@ namespace Barotrauma { voiceChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume * 20.0f, 0); + GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume * 30.0f, 0); #endif } } @@ -215,7 +216,7 @@ namespace Barotrauma get { return microphoneVolume; } set { - microphoneVolume = MathHelper.Clamp(value, 0.1f, 5.0f); + microphoneVolume = MathHelper.Clamp(value, 0.2f, 10.0f); } } public string Language @@ -224,26 +225,45 @@ namespace Barotrauma set { TextManager.Language = value; } } - public readonly HashSet SelectedContentPackages = new HashSet(); + public readonly List SelectedContentPackages = new List(); + + public void SelectContentPackage(ContentPackage contentPackage) + { + if (!SelectedContentPackages.Contains(contentPackage)) + { + SelectedContentPackages.Add(contentPackage); + ContentPackage.SortContentPackages(); + } + } + + public void DeselectContentPackage(ContentPackage contentPackage) + { + if (SelectedContentPackages.Contains(contentPackage)) + { + SelectedContentPackages.Remove(contentPackage); + ContentPackage.SortContentPackages(); + } + } private HashSet selectedContentPackagePaths = new HashSet(); public string MasterServerUrl { get; set; } + public string RemoteContentUrl { get; set; } public bool AutoCheckUpdates { get; set; } public bool WasGameUpdated { get; set; } - private string defaultPlayerName; - public string DefaultPlayerName + private string playerName; + public string PlayerName { get { - return defaultPlayerName ?? ""; + return string.IsNullOrWhiteSpace(playerName) ? Steam.SteamManager.GetUsername() : playerName; } set { - if (defaultPlayerName != value) + if (playerName != value) { - defaultPlayerName = value; + playerName = value; } } } @@ -421,11 +441,11 @@ namespace Barotrauma GraphicsWidth = 1024; GraphicsHeight = 768; MasterServerUrl = ""; - SelectedContentPackages.Add(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage("")); + SelectContentPackage(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage("")); jobPreferences = new List(); - foreach (JobPrefab job in JobPrefab.List) + foreach (string job in JobPrefab.List.Keys) { - jobPreferences.Add(job.Identifier); + jobPreferences.Add(job); } return; } @@ -435,6 +455,7 @@ namespace Barotrauma SetDefaultBindings(doc, legacy: false); MasterServerUrl = doc.Root.GetAttributeString("masterserverurl", MasterServerUrl); + RemoteContentUrl = doc.Root.GetAttributeString("remotecontenturl", RemoteContentUrl); WasGameUpdated = doc.Root.GetAttributeBool("wasgameupdated", WasGameUpdated); VerboseLogging = doc.Root.GetAttributeBool("verboselogging", VerboseLogging); SaveDebugConsoleLogs = doc.Root.GetAttributeBool("savedebugconsolelogs", SaveDebugConsoleLogs); @@ -465,6 +486,7 @@ namespace Barotrauma doc.Root.Add( new XAttribute("language", TextManager.Language), new XAttribute("masterserverurl", MasterServerUrl), + new XAttribute("remotecontenturl", RemoteContentUrl), new XAttribute("autocheckupdates", AutoCheckUpdates), new XAttribute("musicvolume", musicVolume), new XAttribute("soundvolume", soundVolume), @@ -556,7 +578,7 @@ namespace Barotrauma doc.Root.Add(gameplay); var playerElement = new XElement("player", - new XAttribute("name", defaultPlayerName ?? ""), + new XAttribute("name", playerName ?? ""), new XAttribute("headindex", CharacterHeadIndex), new XAttribute("gender", CharacterGender), new XAttribute("race", CharacterRace), @@ -653,6 +675,7 @@ namespace Barotrauma { var missingPackagePaths = new List(); var incompatiblePackages = new List(); + var invalidPackages = new List(); SelectedContentPackages.Clear(); foreach (string path in contentPackagePaths) { @@ -664,24 +687,35 @@ namespace Barotrauma } else if (!matchingContentPackage.IsCompatible()) { + DebugConsole.NewMessage( + $"Content package \"{matchingContentPackage.Name}\" is not compatible with this version of Barotrauma (game version: {GameMain.Version}, content package version: {matchingContentPackage.GameVersion})", + Color.Red); incompatiblePackages.Add(matchingContentPackage); } + else if (!matchingContentPackage.CheckValidity(out List errorMessages)) + { + DebugConsole.NewMessage( + $"Content package \"{matchingContentPackage.Name}\" is invalid: " + string.Join(", ", errorMessages), + Color.Red); + invalidPackages.Add(matchingContentPackage); + //never consider the vanilla content package invalid + //(otherwise a player might brick the game by, for example, deleting vanilla content files) + if (matchingContentPackage == GameMain.VanillaContent) + { + SelectedContentPackages.Add(matchingContentPackage); + } + } else { SelectedContentPackages.Add(matchingContentPackage); } } + ContentPackage.SortContentPackages(); TextManager.LoadTextPacks(SelectedContentPackages); foreach (ContentPackage contentPackage in SelectedContentPackages) { - bool packageOk = contentPackage.VerifyFiles(out List errorMessages); - if (!packageOk) - { - DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\":\n" + string.Join("\n", errorMessages)); - continue; - } foreach (ContentFile file in contentPackage.Files) { ToolBox.IsProperFilenameCase(file.Path); @@ -691,7 +725,7 @@ namespace Barotrauma EnsureCoreContentPackageSelected(); //save to get rid of the invalid selected packages in the config file - if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0) { SaveNewPlayerConfig(); } + if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0 || invalidPackages.Count > 0) { SaveNewPlayerConfig(); } //display error messages after all content packages have been loaded //to make sure the package that contains text files has been loaded before we attempt to use TextManager @@ -699,10 +733,15 @@ namespace Barotrauma { DebugConsole.ThrowError(TextManager.GetWithVariable("ContentPackageNotFound", "[packagepath]", missingPackagePath)); } + foreach (ContentPackage invalidPackage in invalidPackages) + { + DebugConsole.ThrowError(TextManager.GetWithVariable("InvalidContentPackage", "[packagename]", invalidPackage.Name), createMessageBox: true); + } foreach (ContentPackage incompatiblePackage in incompatiblePackages) { DebugConsole.ThrowError(TextManager.GetWithVariables(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { incompatiblePackage.Name, incompatiblePackage.GameVersion.ToString(), GameMain.Version.ToString() })); + new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { incompatiblePackage.Name, incompatiblePackage.GameVersion.ToString(), GameMain.Version.ToString() }), + createMessageBox: true); } } @@ -712,14 +751,14 @@ namespace Barotrauma if (GameMain.VanillaContent != null) { - SelectedContentPackages.Add(GameMain.VanillaContent); + SelectContentPackage(GameMain.VanillaContent); } else { var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage); if (availablePackage != null) { - SelectedContentPackages.Add(availablePackage); + SelectContentPackage(availablePackage); } } } @@ -837,7 +876,9 @@ namespace Barotrauma doc.Root.Add(keyMappingElement); for (int i = 0; i < keyMapping.Length; i++) { - if (keyMapping[i].MouseButton == null) + var key = keyMapping[i]; + if (key == null) { continue; } + if (key.MouseButton == null) { keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key)); } @@ -857,7 +898,7 @@ namespace Barotrauma doc.Root.Add(gameplay); var playerElement = new XElement("player", - new XAttribute("name", defaultPlayerName ?? ""), + new XAttribute("name", playerName ?? ""), new XAttribute("headindex", CharacterHeadIndex), new XAttribute("gender", CharacterGender), new XAttribute("race", CharacterRace), @@ -945,7 +986,7 @@ namespace Barotrauma XElement playerElement = doc.Root.Element("player"); if (playerElement != null) { - defaultPlayerName = playerElement.GetAttributeString("name", defaultPlayerName); + playerName = playerElement.GetAttributeString("name", playerName); CharacterHeadIndex = playerElement.GetAttributeInt("headindex", CharacterHeadIndex); if (Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender g)) { @@ -1041,7 +1082,7 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "contentpackage": - string path = System.IO.Path.GetFullPath(subElement.GetAttributeString("path", "")); + string path = Path.GetFullPath(subElement.GetAttributeString("path", "")); selectedContentPackagePaths.Add(path); break; } @@ -1102,7 +1143,7 @@ namespace Barotrauma VoiceSetting = VoiceMode.Disabled; VoiceCaptureDevice = null; NoiseGateThreshold = -45; - windowMode = WindowMode.Fullscreen; + windowMode = WindowMode.BorderlessWindowed; losMode = LosMode.Transparent; useSteamMatchmaking = true; requireSteamAuthentication = true; @@ -1123,9 +1164,9 @@ namespace Barotrauma DynamicRangeCompressionEnabled = true; VoipAttenuationEnabled = true; voiceChatVolume = 0.5f; - microphoneVolume = 1.0f; + microphoneVolume = 5.0f; AutoCheckUpdates = true; - defaultPlayerName = string.Empty; + playerName = string.Empty; HUDScale = 1; InventoryScale = 1; AutoUpdateWorkshopItems = true; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs index 77eb57a86..997f95df2 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs @@ -42,17 +42,17 @@ namespace Barotrauma.Items.Components public int DockingDir { get; private set; } - [Serialize("32.0,32.0", false)] + [Serialize("32.0,32.0", false, description: "How close the docking port has to be to another port to dock.")] public Vector2 DistanceTolerance { get; set; } - [Serialize(32.0f, false)] + [Serialize(32.0f, false, description: "How close together the docking ports are forced when docked.")] public float DockedDistance { get; set; } - [Serialize(true, false)] + [Serialize(true, false, description: "Is the port horizontal.")] public bool IsHorizontal { get; @@ -189,9 +189,7 @@ namespace Barotrauma.Items.Components GameMain.GameScreen.Cam.Shake = Vector2.Distance(DockingTarget.item.Submarine.Velocity, item.Submarine.Velocity); } - DockingDir = IsHorizontal ? - Math.Sign(DockingTarget.item.WorldPosition.X - item.WorldPosition.X) : - Math.Sign(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y); + DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; if (door != null && DockingTarget.door != null) @@ -230,9 +228,7 @@ namespace Barotrauma.Items.Components if (!(joint is WeldJoint)) { - DockingDir = IsHorizontal ? - Math.Sign(DockingTarget.item.WorldPosition.X - item.WorldPosition.X) : - Math.Sign(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y); + DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; ApplyStatusEffects(ActionType.OnUse, 1.0f); @@ -312,7 +308,7 @@ namespace Barotrauma.Items.Components joint.CollideConnected = true; } - public int GetDir() + public int GetDir(DockingPort dockingTarget = null) { if (DockingDir != 0) { return DockingDir; } @@ -325,7 +321,12 @@ namespace Barotrauma.Items.Components Math.Sign(door.Item.WorldPosition.Y - door.LinkedGap.linkedTo[0].WorldPosition.Y); } } - + if (dockingTarget != null) + { + return IsHorizontal ? + Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) : + Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y); + } if (item.Submarine != null) { return IsHorizontal ? @@ -964,57 +965,5 @@ namespace Barotrauma.Items.Components msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null); } } - - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) - { - bool isDocked = msg.ReadBoolean(); - - for (int i = 0; i < 2; i++) - { - if (hulls[i] == null) continue; - item.linkedTo.Remove(hulls[i]); - hulls[i].Remove(); - hulls[i] = null; - } - - if (gap != null) - { - item.linkedTo.Remove(gap); - gap.Remove(); - gap = null; - } - - if (isDocked) - { - ushort dockingTargetID = msg.ReadUInt16(); - - bool isLocked = msg.ReadBoolean(); - - Entity targetEntity = Entity.FindEntityByID(dockingTargetID); - if (targetEntity == null || !(targetEntity is Item)) - { - DebugConsole.ThrowError("Invalid docking port network event (can't dock to " + targetEntity.ToString() + ")"); - return; - } - - DockingTarget = (targetEntity as Item).GetComponent(); - if (DockingTarget == null) - { - DebugConsole.ThrowError("Invalid docking port network event (" + targetEntity + " doesn't have a docking port component)"); - return; - } - - Dock(DockingTarget); - - if (isLocked) - { - Lock(isNetworkMessage: true, forcePosition: true); - } - } - else - { - Undock(); - } - } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs index 82c04b75a..136b9513f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs @@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components public bool CanBeWelded = true; private float stuck; - [Serialize(0.0f, false)] + [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.")] public float Stuck { get { return stuck; } @@ -74,10 +74,10 @@ namespace Barotrauma.Items.Components } } - [Serialize(3.0f, true), Editable] + [Serialize(3.0f, true, description: "How quickly the door opens."), Editable] public float OpeningSpeed { get; private set; } - [Serialize(3.0f, true), Editable] + [Serialize(3.0f, true, description: "How quickly the door closes."), Editable] public float ClosingSpeed { get; private set; } public bool? PredictedState { get; private set; } @@ -121,10 +121,10 @@ namespace Barotrauma.Items.Components public bool IsHorizontal { get; private set; } - [Serialize("0.0,0.0,0.0,0.0", false)] + [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.")] public Rectangle Window { get; set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, true, description: "Is the door currently open.")] public bool IsOpen { get { return isOpen; } @@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false)] + [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).")] public bool HasIntegratedButtons { get; private set; } public float OpenState @@ -153,7 +153,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false)] + [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.")] public bool Impassable { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs index 001155e91..df42f6c2e 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs @@ -48,28 +48,28 @@ namespace Barotrauma.Items.Components } } - [Serialize(100.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] + [Serialize(100.0f, true, description: "How far the discharge can travel from the item."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] public float Range { get; set; } - [Serialize(10.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, ToolTip = "How much further can the discharge be carried when moving across walls.")] + [Serialize(10.0f, true, description: "How much further can the discharge be carried when moving across walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float RangeMultiplierInWalls { get; set; } - [Serialize(0.25f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Duration { get; set; } - [Serialize(false, true), Editable()] + [Serialize(false, true, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] public bool OutdoorsOnly { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs index a893e97ea..ccd091eb2 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components get { return item.body ?? body; } } - [Serialize(false, true)] + [Serialize(false, true, 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; } @@ -50,56 +50,58 @@ namespace Barotrauma.Items.Components } } - [Serialize(true, true)] + [Serialize(true, true, 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)] + [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.")] public bool ControlPose { get; set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item be attached to walls.")] public bool Attachable { get { return attachable; } set { attachable = value; } } - [Serialize(true, false)] + [Serialize(true, false, 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)] + [Serialize(false, false, 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; } } - [Serialize("0.0,0.0", false),Editable] + [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)."+ + " 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 { get { return ConvertUnits.ToDisplayUnits(holdPos); } set { holdPos = ConvertUnits.ToSimUnits(value); } } - [Serialize("0.0,0.0", false)] + [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)."+ + " 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 { get { return ConvertUnits.ToDisplayUnits(aimPos); } set { aimPos = ConvertUnits.ToSimUnits(value); } } - [Serialize(0.0f, false), Editable] + [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).")] public float HoldAngle { get { return MathHelper.ToDegrees(holdAngle); } @@ -107,21 +109,21 @@ namespace Barotrauma.Items.Components } private Vector2 swingAmount; - [Serialize("0.0,0.0", false), Editable] + [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).")] public Vector2 SwingAmount { get { return ConvertUnits.ToDisplayUnits(swingAmount); } set { swingAmount = ConvertUnits.ToSimUnits(value); } } - [Serialize(0.0f, false), Editable] + [Editable, Serialize(0.0f, false, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] public float SwingSpeed { get; set; } - [Serialize(false, false), Editable] + [Editable, Serialize(false, false, description: "Should the item swing around when it's being held.")] public bool SwingWhenHolding { get; set; } - [Serialize(false, false), Editable] + [Editable, Serialize(false, false, description: "Should the item swing around when it's being aimed.")] public bool SwingWhenAiming { get; set; } - [Serialize(false, false), Editable] + [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).")] public bool SwingWhenUsing { get; set; } public Holdable(Item item, XElement element) @@ -189,9 +191,16 @@ namespace Barotrauma.Items.Components } } - public override void Load(XElement componentElement) + public override void Load(XElement componentElement, bool usePrefabValues) { - base.Load(componentElement); + base.Load(componentElement, usePrefabValues); + + if (usePrefabValues) + { + //this needs to be loaded regardless + Attached = componentElement.GetAttributeBool("attached", attached); + } + if (attachable) { prevMsg = DisplayMsg; @@ -221,24 +230,24 @@ namespace Barotrauma.Items.Components item.body = body; } } - - if (Pusher != null) Pusher.Enabled = false; - if (item.body != null) item.body.Enabled = true; + + if (Pusher != null) { Pusher.Enabled = false; } + if (item.body != null){ item.body.Enabled = true; } IsActive = false; if (picker == null) { - if (dropper == null) return; + if (dropper == null) { return; } picker = dropper; } - if (picker.Inventory == null) return; + if (picker.Inventory == null) { return; } item.Submarine = picker.Submarine; if (item.body != null) { item.body.ResetDynamics(); - Limb heldHand; - Limb arm; + Limb heldHand, arm; + Vector2 diff = Vector2.Zero; if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) { heldHand = picker.AnimController.GetLimb(LimbType.LeftHand); @@ -249,11 +258,18 @@ namespace Barotrauma.Items.Components heldHand = picker.AnimController.GetLimb(LimbType.RightHand); arm = picker.AnimController.GetLimb(LimbType.RightArm); } - - float xDif = (heldHand.SimPosition.X - arm.SimPosition.X) / 2f; - float yDif = (heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f; - //hand simPosition is actually in the wrist so need to move the item out from it slightly - item.SetTransform(heldHand.SimPosition + new Vector2(xDif, yDif), 0.0f); + if (heldHand != null && arm != null) + { + //hand simPosition is actually in the wrist so need to move the item out from it slightly + diff = new Vector2( + (heldHand.SimPosition.X - arm.SimPosition.X) / 2f, + (heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f); + item.SetTransform(heldHand.SimPosition + diff, 0.0f); + } + else + { + item.SetTransform(picker.SimPosition, 0.0f); + } } picker.DeselectItem(item); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs index 547ebde12..c9c039e6c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs @@ -16,14 +16,14 @@ namespace Barotrauma.Items.Components private float deattachTimer; - [Serialize(1.0f, false)] + [Serialize(1.0f, false, description: "How long it takes to deattach the item from the level walls (in seconds).")] public float DeattachDuration { get; set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, false, 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; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs index f41fa5226..49b5e065f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs @@ -15,38 +15,32 @@ namespace Barotrauma.Items.Components private bool hitting; - private Attack attack; - private float range; - - private Character user; - private float reload; private float reloadTimer; - private HashSet hitTargets = new HashSet(); + private readonly Attack attack; - public Character User - { - get { return user; } - } + private readonly HashSet hitTargets = new HashSet(); - [Serialize(0.0f, false)] + 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.")] public float Range { get { return ConvertUnits.ToDisplayUnits(range); } set { range = ConvertUnits.ToSimUnits(value); } } - [Serialize(0.5f, false)] + [Serialize(0.5f, false, 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)] + [Serialize(false, false, description: "Can the weapon hit multiple targets per swing.")] public bool AllowHitMultiple { get; @@ -85,6 +79,7 @@ namespace Barotrauma.Items.Components if (hitPos < MathHelper.PiOver4) { return false; } + ActivateNearbySleepingCharacters(); reloadTimer = reload; item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; @@ -162,7 +157,7 @@ namespace Barotrauma.Items.Components { hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f); ac.HoldItem(deltaTime, item, handlePos, new Vector2(2, 0), Vector2.Zero, false, hitPos, holdAngle + hitPos); // aimPos not used -> zero (new Vector2(-0.3f, 0.2f)), holdPos new Vector2(0.6f, -0.1f) - if (hitPos < -MathHelper.PiOver4 * 1.2f) + if (hitPos < -MathHelper.PiOver2) { RestoreCollision(); hitting = false; @@ -172,12 +167,36 @@ namespace Barotrauma.Items.Components } } + /// + /// Activate sleeping ragdolls that are close enough to hit with the weapon (otherwise the collision will not be registered) + /// + private void ActivateNearbySleepingCharacters() + { + foreach (Character c in Character.CharacterList) + { + if (!c.Enabled || !c.AnimController.BodyInRest) { continue; } + //do a broad check first + if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; } + if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; } + + foreach (Limb limb in c.AnimController.Limbs) + { + float hitRange = 2.0f; + if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < hitRange * hitRange) + { + c.AnimController.BodyInRest = false; + break; + } + } + } + } + private void SetUser(Character character) { - if (user == character) { return; } - if (user != null && user.Removed) { user = null; } + if (User == character) { return; } + if (User != null && User.Removed) { User = null; } - user = character; + User = character; if (item.body?.FarseerBody == null || item.Removed || !GameMain.World.BodyList.Contains(item.body.FarseerBody)) @@ -185,9 +204,9 @@ namespace Barotrauma.Items.Components return; } - if (user != null) + if (User != null) { - foreach (Limb limb in user.AnimController.Limbs) + foreach (Limb limb in User.AnimController.Limbs) { if (limb.body.FarseerBody != null && GameMain.World.BodyList.Contains(limb.body.FarseerBody)) { @@ -216,18 +235,18 @@ namespace Barotrauma.Items.Components private bool OnCollision(Fixture f1, Fixture f2, Contact contact) { - if (user == null || user.Removed) + if (User == null || User.Removed) { RestoreCollision(); hitting = false; - user = null; + User = null; } Character targetCharacter = null; Limb targetLimb = null; Structure targetStructure = null; - attack?.SetUser(user); + attack?.SetUser(User); if (f2.Body.UserData is Limb) { @@ -283,16 +302,16 @@ namespace Barotrauma.Items.Components if (targetLimb != null) { targetLimb.character.LastDamageSource = item; - attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f); + attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f); } else if (targetCharacter != null) { targetCharacter.LastDamageSource = item; - attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f); + attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); } else if (targetStructure != null) { - attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f); + attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); } else { @@ -326,7 +345,7 @@ namespace Barotrauma.Items.Components if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? { - ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user); + ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Propulsion.cs index 088e335cd..82e8c5b4c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Propulsion.cs @@ -10,27 +10,22 @@ namespace Barotrauma.Items.Components { class Propulsion : ItemComponent { - enum UsableIn + public enum UseEnvironment { Air, Water, Both }; - private float force; - private float useState; - - private UsableIn usableIn; - [Serialize(0.0f, false), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] - public float Force - { - get { return force; } - set { force = value; } - } + [Serialize(UseEnvironment.Both, false, 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)] + public float Force { get; set; } #if CLIENT private string particles; - [Serialize("", false)] + [Serialize("", false, description: "The name of the particle prefab the item emits when used.")] public string Particles { get { return particles; } @@ -41,19 +36,6 @@ namespace Barotrauma.Items.Components public Propulsion(Item item, XElement element) : base(item,element) { - switch (element.GetAttributeString("usablein", "both").ToLowerInvariant()) - { - case "air": - usableIn = UsableIn.Air; - break; - case "water": - usableIn = UsableIn.Water; - break; - case "both": - default: - usableIn = UsableIn.Both; - break; - } ResetSoundRange(); } @@ -67,18 +49,18 @@ namespace Barotrauma.Items.Components if (character.AnimController.InWater) { - if (usableIn == UsableIn.Air) return true; + if (UsableIn == UseEnvironment.Air) return true; } else { - if (usableIn == UsableIn.Water) return true; + if (UsableIn == UseEnvironment.Water) return true; } Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position); //move upwards if the cursor is at the position of the character if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY; - Vector2 propulsion = dir * force; + Vector2 propulsion = dir * Force; if (character.AnimController.InWater) character.AnimController.TargetMovement = dir; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RangedWeapon.cs index 1a60462f0..3a4439e33 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RangedWeapon.cs @@ -15,28 +15,28 @@ namespace Barotrauma.Items.Components private Vector2 barrelPos; - [Serialize("0.0,0.0", false)] + [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.")] public string BarrelPos { get { return XMLExtensions.Vector2ToString(ConvertUnits.ToDisplayUnits(barrelPos)); } set { barrelPos = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(value)); } } - [Serialize(1.0f, false)] + [Serialize(1.0f, false, 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(0.0f, false)] + [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).")] public float Spread { get; set; } - [Serialize(0.0f, false)] + [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).")] public float UnskilledSpread { get; @@ -109,30 +109,21 @@ namespace Barotrauma.Items.Components { foreach (Item item in containedItems) { - projectile = item.GetComponent(); - if (projectile != null) break; - } - //projectile not found, see if one of the contained items contains projectiles - if (projectile == null) - { - foreach (Item item in containedItems) + var containedSubItems = item.ContainedItems; + if (containedSubItems == null) { continue; } + foreach (Item subItem in containedSubItems) { - var containedSubItems = item.ContainedItems; - if (containedSubItems == null) { continue; } - foreach (Item subItem in containedSubItems) + projectile = subItem.GetComponent(); + //apply OnUse statuseffects to the container in case it has to react to it somehow + //(play a sound, spawn more projectiles, reduce condition...) + if (subItem.Condition > 0.0f) { - projectile = subItem.GetComponent(); - //apply OnUse statuseffects to the container in case it has to react to it somehow - //(play a sound, spawn more projectiles, reduce condition...) - if (subItem.Condition > 0.0f) - { - subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, deltaTime); - } - if (projectile != null) break; + subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, deltaTime); } + if (projectile != null) break; } } - } + } if (projectile == null) return true; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs index 83455c182..9b38863f3 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs @@ -6,9 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; -#if CLIENT -using Barotrauma.Particles; -#endif namespace Barotrauma.Items.Components { @@ -25,41 +22,58 @@ namespace Barotrauma.Items.Components private Vector2 debugRayStartPos, debugRayEndPos; - [Serialize("Both", false)] + [Serialize("Both", false, description: "Can the item be used in air, water or both.")] public UseEnvironment UsableIn { get; set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, false, description: "The distance at which the item can repair targets.")] public float Range { get; set; } - [Serialize(0.0f, false)] + [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).")] + 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).")] + public float UnskilledSpread + { + get; + set; + } + + [Serialize(0.0f, false, description: "How many units of damage the item removes from structures per second.")] public float StructureFixAmount { get; set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")] public float ExtinguishAmount { get; set; } - [Serialize("0.0,0.0", false)] + [Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")] public Vector2 BarrelPos { get; set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item repair things through walls.")] public bool RepairThroughWalls { get; set; } - [Serialize(false, false)] + [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.")] public bool RepairMultiple { get; set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } - [Serialize(0.0f, false)] + [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).")] public float FireProbability { get; set; } + [Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")] + public float TargetForce { get; set; } + public Vector2 TransformedBarrelPos { get @@ -164,10 +178,12 @@ namespace Barotrauma.Items.Components if (item.Submarine != null) { rayStart += item.Submarine.SimPosition; } } + float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess)); + float angle = item.body.Rotation + spread * Rand.Range(-0.5f, 0.5f); Vector2 rayEnd = rayStart + ConvertUnits.ToSimUnits(new Vector2( - (float)Math.Cos(item.body.Rotation), - (float)Math.Sin(item.body.Rotation)) * Range * item.body.Dir); + (float)Math.Cos(angle), + (float)Math.Sin(angle)) * Range * item.body.Dir); List ignoredBodies = new List(); foreach (Limb limb in character.AnimController.Limbs) @@ -319,6 +335,7 @@ namespace Barotrauma.Items.Components if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; } + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure }); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); targetStructure.AddDamage(sectionIndex, -StructureFixAmount * degreeOfSuccess, user); @@ -341,15 +358,43 @@ namespace Barotrauma.Items.Components { if (targetCharacter.Removed) { return false; } targetCharacter.LastDamageSource = item; - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new List() { targetCharacter }); + Limb closestLimb = null; + float closestDist = float.MaxValue; + foreach (Limb limb in targetCharacter.AnimController.Limbs) + { + float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition); + if (dist < closestDist) + { + closestLimb = limb; + closestDist = dist; + } + } + + if (closestLimb != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) + { + Vector2 dir = closestLimb.WorldPosition - item.WorldPosition; + dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); + closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); + } + + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, + closestLimb == null ? new ISerializableEntity[] { targetCharacter } : new ISerializableEntity[] { targetCharacter, closestLimb }); FixCharacterProjSpecific(user, deltaTime, targetCharacter); return true; } else if (targetBody.UserData is Limb targetLimb) { if (targetLimb.character == null || targetLimb.character.Removed) { return false; } + + if (!MathUtils.NearlyEqual(TargetForce, 0.0f)) + { + Vector2 dir = targetLimb.WorldPosition - item.WorldPosition; + dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); + targetLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); + } + targetLimb.character.LastDamageSource = item; - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new List() { targetLimb.character, targetLimb }); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetLimb.character, targetLimb }); FixCharacterProjSpecific(user, deltaTime, targetLimb.character); return true; } @@ -359,6 +404,13 @@ namespace Barotrauma.Items.Components ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); + if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) + { + Vector2 dir = targetItem.WorldPosition - item.WorldPosition; + dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); + targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); + } + var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.IsActive && levelResource.requiredItems.Any() && @@ -509,6 +561,15 @@ namespace Barotrauma.Items.Components { effect.Apply(actionType, deltaTime, item, targets); } + else if (effect.HasTargetType(StatusEffect.TargetType.Character)) + { + effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Character)); + } + else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) + { + effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Limb)); + } + #if CLIENT // Hard-coded progress bars for welding doors stuck. // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs index e3f4217c7..336303f42 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components private bool midAir; - [Serialize(1.0f, false)] + [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.")] public float ThrowForce { get { return throwForce; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs index c07b58a9f..0bedaa73c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components /// Vector2 DrawSize { get; } - void Draw(SpriteBatch spriteBatch, bool editing); + void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1); #endif } @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components protected CoroutineHandle delayedCorrectionCoroutine; protected float correctionTimer; - [Editable, Serialize(0.0f, false)] + [Editable, Serialize(0.0f, false, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime { get; @@ -114,45 +114,42 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, false)] //Editable for doors to do their magic + [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 public bool CanBePicked { get { return canBePicked; } set { canBePicked = value; } } - [Serialize(false, false)] + [Serialize(false, false, description: "Should the interface of the item (if it has one) be drawn when the item is equipped.")] public bool DrawHudWhenEquipped { get; private set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item be selected by interacting with it.")] public bool CanBeSelected { get { return canBeSelected; } set { canBeSelected = value; } } - //Transfer conditions between same prefab items - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item be combined with other items of the same type.")] public bool CanBeCombined { get { return canBeCombined; } set { canBeCombined = value; } } - //Remove item if combination results in 0 condition - [Serialize(false, false)] + [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.")] public bool RemoveOnCombined { get { return removeOnCombined; } set { removeOnCombined = value; } } - //Can the "Use" action be triggered by characters or just other items/statuseffects - [Serialize(false, false)] + [Serialize(false, false, description: "Can the \"Use\" action of the item be triggered by characters or just other items/StatusEffects.")] public bool CharacterUsable { get { return characterUsable; } @@ -160,7 +157,7 @@ namespace Barotrauma.Items.Components } //Remove item if combination results in 0 condition - [Serialize(true, false), Editable(ToolTip = "Can the properties of the component be edited in-game (only applicable if the component has in-game editable properties).")] + [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()] public bool AllowInGameEditing { get; @@ -179,7 +176,7 @@ namespace Barotrauma.Items.Components protected set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Should the item be deleted when it's used.")] public bool DeleteOnUse { get; @@ -196,7 +193,7 @@ namespace Barotrauma.Items.Components get { return name; } } - [Editable, Serialize("", true, translationTextTag: "ItemMsg")] + [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\").")] public string Msg { get; @@ -213,7 +210,7 @@ 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)] + [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).")] public float CombatPriority { get; private set; } public ItemComponent(Item item, XElement element) @@ -400,7 +397,7 @@ namespace Barotrauma.Items.Components } } - public virtual bool Combine(Item item) + public virtual bool Combine(Item item, Character user) { if (canBeCombined && this.item.Prefab == item.Prefab && item.Condition > 0.0f && this.item.Condition > 0.0f) { @@ -670,9 +667,9 @@ namespace Barotrauma.Items.Components } } - public virtual void Load(XElement componentElement) + public virtual void Load(XElement componentElement, bool usePrefabValues) { - if (componentElement == null) return; + if (componentElement == null || usePrefabValues) { return; } foreach (XAttribute attribute in componentElement.Attributes()) { if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) continue; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs index 6266d4111..dbd110d73 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs @@ -8,7 +8,6 @@ namespace Barotrauma.Items.Components { partial class ItemContainer : ItemComponent, IDrawableComponent { - private List containableItems; public ItemInventory Inventory; private List> itemsWithStatusEffects; @@ -17,7 +16,7 @@ namespace Barotrauma.Items.Components //how many items can be contained private int capacity; - [Serialize(5, false)] + [Serialize(5, false, description: "How many items can be contained inside this item.")] public int Capacity { get { return capacity; } @@ -25,18 +24,19 @@ namespace Barotrauma.Items.Components } private bool hideItems; - [Serialize(true, false)] + [Serialize(true, false, 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 { get { return hideItems; } - set - { + set + { hideItems = value; Drawable = !hideItems; } } - [Serialize(true, false)] + [Serialize(true, false, description: "Should the inventory of this item be visible when the item is selected.")] public bool DrawInventory { get; @@ -44,28 +44,23 @@ namespace Barotrauma.Items.Components } - [Serialize(false, false)] + [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.")] public bool AutoInteractWithContained { get; set; } - [Serialize("0.5,0.5", false)] - public Vector2 HudPos { get; set; } - [Serialize(5, false)] + [Serialize(5, false, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } - public List ContainableItems - { - get { return containableItems; } - } + public List ContainableItems { get; private set; } public ItemContainer(Item item, XElement element) : base (item, element) { - Inventory = new ItemInventory(item, this, capacity, HudPos, SlotsPerRow); - containableItems = new List(); + Inventory = new ItemInventory(item, this, capacity, SlotsPerRow); + ContainableItems = new List(); foreach (XElement subElement in element.Elements()) { @@ -78,7 +73,7 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); continue; } - containableItems.Add(containable); + ContainableItems.Add(containable); break; } } @@ -94,7 +89,7 @@ namespace Barotrauma.Items.Components { item.SetContainedItemPositions(); - RelatedItem ri = containableItems.Find(x => x.MatchesItem(containedItem)); + RelatedItem ri = ContainableItems.Find(x => x.MatchesItem(containedItem)); if (ri != null) { itemsWithStatusEffects.RemoveAll(i => i.First == containedItem); @@ -118,8 +113,8 @@ namespace Barotrauma.Items.Components public bool CanBeContained(Item item) { - if (containableItems.Count == 0) return true; - return (containableItems.Find(x => x.MatchesItem(item)) != null); + if (ContainableItems.Count == 0) return true; + return (ContainableItems.Find(x => x.MatchesItem(item)) != null); } public override void Update(float deltaTime, Camera cam) @@ -189,9 +184,10 @@ namespace Barotrauma.Items.Components return (picker != null); } - public override bool Combine(Item item) + public override bool Combine(Item item, Character user) { - if (!containableItems.Any(x => x.MatchesItem(item))) return false; + if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; } + if (user != null && !user.CanAccessInventory(Inventory)) { return false; } if (Inventory.TryPutItem(item, null)) { @@ -286,20 +282,16 @@ namespace Barotrauma.Items.Components } } - public override void Load(XElement componentElement) + public override void Load(XElement componentElement, bool usePrefabValues) { - base.Load(componentElement); + base.Load(componentElement, usePrefabValues); string containedString = componentElement.GetAttributeString("contained", ""); - string[] itemIdStrings = containedString.Split(','); - itemIds = new ushort[itemIdStrings.Length]; for (int i = 0; i < itemIdStrings.Length; i++) { - ushort id = 0; - if (!ushort.TryParse(itemIdStrings[i], out id)) continue; - + if (!ushort.TryParse(itemIdStrings[i], out ushort id)) { continue; } itemIds[i] = id; } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Controller.cs index 28ae29b89..319ea9f53 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Controller.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components partial class Controller : ItemComponent, IServerSerializable { //where the limbs of the user should be positioned when using the controller - private List limbPositions; + private readonly List limbPositions; private Direction dir; @@ -51,7 +51,9 @@ namespace Barotrauma.Items.Components get { return user; } } - [Serialize(false, false), Editable(ToolTip = "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.")] + 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.")] public bool IsToggle { get; @@ -146,7 +148,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, user); - if (limbPositions.Count == 0) return; + if (limbPositions.Count == 0) { return; } user.AnimController.Anim = AnimController.Animation.UsingConstruction; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs index 1bfa76d6d..2f3d61d1a 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs @@ -22,8 +22,8 @@ namespace Barotrauma.Items.Components private float prevVoltage; - [Editable(0.0f, 10000000.0f, ToolTip = "The amount of force exerted on the submarine when the engine is operating at full power."), - Serialize(2000.0f, true)] + [Editable(0.0f, 10000000.0f), + Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { get { return maxForce; } @@ -33,7 +33,9 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize("0.0,0.0", true)] + [Editable, Serialize("0.0,0.0", true, + 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 { get; @@ -148,6 +150,16 @@ namespace Barotrauma.Items.Components force = MathHelper.Lerp(force, 0.0f, 0.1f); } + public override void FlipX(bool relativeToSub) + { + PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); + } + + public override void FlipY(bool relativeToSub) + { + PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); + } + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs index 18b2c7f55..627e7b1b1 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs @@ -22,23 +22,23 @@ namespace Barotrauma.Items.Components private bool hasPower; - private Dictionary hullDatas; + private readonly Dictionary hullDatas; - [Editable(ToolTip = "Does the machine require inputs from water detectors in order to show the water levels inside rooms."), Serialize(false, true)] + [Editable, Serialize(false, true, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")] public bool RequireWaterDetectors { get; set; } - [Editable(ToolTip = "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms."), Serialize(true, true)] + [Editable, Serialize(true, true, description: "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms.")] public bool RequireOxygenDetectors { get; set; } - [Editable(ToolTip = "Should damaged walls be displayed by the machine."), Serialize(true, true)] + [Editable, Serialize(true, true, description: "Should damaged walls be displayed by the machine.")] public bool ShowHullIntegrity { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs index 59d404be7..702ee23cb 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components private set; } - [Editable(ToolTip = "How much oxygen the machine generates when operating at full power."), Serialize(400.0f, true)] + [Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.")] public float GeneratedAmount { get { return generatedAmount; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs index 2f358e309..5bf8dcc7f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components private bool hasPower; - [Serialize(0.0f, true)] + [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).")] public float FlowPercentage { get { return flowPercentage; } @@ -29,7 +29,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(80.0f, false)] + [Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.")] public float MaxFlow { get { return maxFlow; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs index 31c995ea5..76879e86e 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs @@ -51,6 +51,8 @@ namespace Barotrauma.Items.Components const float AIUpdateInterval = 0.2f; private float aiUpdateTimer; + private Character lastAIUser; + private Character lastUser; private Character LastUser { @@ -63,7 +65,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue, ToolTip = "How much power (kW) the reactor generates when operating at full capacity."), Serialize(10000.0f, true)] + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.")] public float MaxPowerOutput { get { return maxPowerOutput; } @@ -73,21 +75,21 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue, ToolTip = "How long the temperature has to stay critical until a meltdown occurs."), Serialize(120.0f, true)] + [Editable(0.0f, float.MaxValue), Serialize(120.0f, true, 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, ToolTip = "How long the temperature has to stay critical until the reactor catches fire."), Serialize(30.0f, true)] + [Editable(0.0f, float.MaxValue), Serialize(30.0f, true, 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)] + [Serialize(0.0f, true, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")] public float Temperature { get { return temperature; } @@ -98,7 +100,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true)] + [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).")] public float FissionRate { get { return fissionRate; } @@ -109,7 +111,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true)] + [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).")] public float TurbineOutput { get { return turbineOutput; } @@ -120,7 +122,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.2f, true), Editable(0.0f, 1000.0f, ToolTip = "How fast the condition of the contained fuel rods deteriorates.")] + [Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f)] public float FuelConsumptionRate { get { return fuelConsumptionRate; } @@ -131,7 +133,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, true)] + [Serialize(false, true, 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; } @@ -143,7 +145,7 @@ namespace Barotrauma.Items.Components private float targetFissionRate; private float targetTurbineOutput; - [Serialize(false, true)] + [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).")] public bool AutoTemp { get { return autoTemp; } @@ -193,6 +195,18 @@ namespace Barotrauma.Items.Components } #endif + //if an AI character was using the item on the previous frame but not anymore, turn autotemp on + // (= bots turn autotemp back on when leaving the reactor) + if (lastAIUser != null) + { + if (lastAIUser.SelectedConstruction != item && lastAIUser.CanInteractWith(item)) + { + AutoTemp = true; + unsentChanges = true; + lastAIUser = null; + } + } + prevAvailableFuel = AvailableFuel; ApplyStatusEffects(ActionType.OnActive, deltaTime, null); @@ -562,7 +576,7 @@ namespace Barotrauma.Items.Components character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f); } - LastUser = character; + LastUser = lastAIUser = character; switch (objective.Option.ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs index 646a953cd..e349fa50e 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs @@ -65,35 +65,37 @@ namespace Barotrauma.Items.Components private bool useDirectionalPing = false; private Vector2 pingDirection = new Vector2(1.0f, 0.0f); - private Sprite pingCircle, directionalPingCircle, screenOverlay, screenBackground; + private Sprite pingCircle, directionalPingCircle; + private Sprite screenOverlay, screenBackground; + private Sprite sonarBlip; private Sprite lineSprite; private bool aiPingCheckPending; //the float value is a timer used for disconnecting the transducer if no signal is received from it for 1 second - private List connectedTransducers; + private readonly List connectedTransducers; public IEnumerable ConnectedTransducers { get { return connectedTransducers.Select(t => t.Transducer); } } - [Serialize(DefaultSonarRange, false)] + [Serialize(DefaultSonarRange, false, description: "The maximum range of the sonar.")] public float Range { get { return range; } set { range = MathHelper.Clamp(value, 0.0f, 100000.0f); } } - [Serialize(false, false)] + [Serialize(false, false, description: "Should the sonar display the walls of the submarine it is inside.")] public bool DetectSubmarineWalls { get; set; } - [Serialize(false, false), Editable(ToolTip = "Does the sonar have to be connected to external transducers to work.")] + [Editable, Serialize(false, false, description: "Does the sonar have to be connected to external transducers to work.")] public bool UseTransducers { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs index d92f80dd6..2c2411e1e 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs @@ -74,9 +74,10 @@ namespace Barotrauma.Items.Components } } } - - [Editable(0.0f, 1.0f, decimals: 3, ToolTip = "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."), Serialize(0.5f, true)] + + [Editable(0.0f, 1.0f, decimals: 3), + Serialize(0.5f, true, 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 { get { return neutralBallastLevel; } @@ -86,7 +87,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(1000.0f, true)] + [Serialize(1000.0f, true, description: "How close the docking port has to be to another docking port for the docking mode to become active.")] public float DockingAssistThreshold { get; @@ -521,98 +522,5 @@ namespace Barotrauma.Items.Components base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength); } } - - public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c) - { - bool autoPilot = msg.ReadBoolean(); - bool dockingButtonClicked = msg.ReadBoolean(); - Vector2 newSteeringInput = targetVelocity; - bool maintainPos = false; - Vector2? newPosToMaintain = null; - bool headingToStart = false; - - if (autoPilot) - { - maintainPos = msg.ReadBoolean(); - if (maintainPos) - { - newPosToMaintain = new Vector2( - msg.ReadSingle(), - msg.ReadSingle()); - } - else - { - headingToStart = msg.ReadBoolean(); - } - } - else - { - newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle()); - } - - if (!item.CanClientAccess(c)) return; - - user = c.Character; - AutoPilot = autoPilot; - - if (dockingButtonClicked) - { - item.SendSignal(0, "1", "toggle_docking", sender: Character.Controlled); - } - - if (!AutoPilot) - { - steeringInput = newSteeringInput; - steeringAdjustSpeed = MathHelper.Lerp(0.2f, 1.0f, c.Character.GetSkillLevel("helm") / 100.0f); - } - else - { - MaintainPos = newPosToMaintain != null; - posToMaintain = newPosToMaintain; - - if (posToMaintain == null) - { - LevelStartSelected = headingToStart; - LevelEndSelected = !headingToStart; - UpdatePath(); - } - else - { - LevelStartSelected = false; - LevelEndSelected = false; - } - } - - //notify all clients of the changed state - unsentChanges = true; - } - - public void ServerWrite(IWriteMessage msg, Barotrauma.Networking.Client c, object[] extraData = null) - { - msg.Write(autoPilot); - - if (!autoPilot) - { - //no need to write steering info if autopilot is controlling - msg.Write(steeringInput.X); - msg.Write(steeringInput.Y); - msg.Write(targetVelocity.X); - msg.Write(targetVelocity.Y); - msg.Write(steeringAdjustSpeed); - } - else - { - msg.Write(posToMaintain != null); - if (posToMaintain != null) - { - msg.Write(((Vector2)posToMaintain).X); - msg.Write(((Vector2)posToMaintain).Y); - } - else - { - msg.Write(LevelStartSelected); - } - } - } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs index 2500c23dd..55e4241d8 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components private float charge; - private float rechargeVoltage, outputVoltage; + private float rechargeVoltage; //how fast the battery can be recharged private float maxRechargeSpeed; @@ -38,39 +38,38 @@ namespace Barotrauma.Items.Components private set; } - [Serialize("0,0", true)] + [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.")] public Vector2 IndicatorPosition { get { return indicatorPosition; } set { indicatorPosition = value; } } - [Serialize("0,0", true)] + [Serialize("0,0", true, 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)] + [Serialize(false, true, 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(ToolTip = "Maximum output of the device when fully charged (kW)."), Serialize(10.0f, true)] + [Editable, Serialize(10.0f, true, description: "Maximum output of the device when fully charged (kW).")] public float MaxOutPut { set; get; } - [Serialize(10.0f, true), Editable(ToolTip = "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, 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.")] public float Capacity { get { return capacity; } set { capacity = Math.Max(value, 1.0f); } } - [Editable, Serialize(0.0f, true)] + [Editable, Serialize(0.0f, true, description: "The current charge of the device.")] public float Charge { get { return charge; } @@ -92,15 +91,14 @@ namespace Barotrauma.Items.Components public float ChargePercentage => MathUtils.Percentage(Charge, Capacity); - [Serialize(10.0f, true), Editable(ToolTip = "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, 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.")] public float MaxRechargeSpeed { get { return maxRechargeSpeed; } set { maxRechargeSpeed = Math.Max(value, 1.0f); } } - [Serialize(10.0f, true), Editable] + [Editable, Serialize(10.0f, true, description: "The current recharge speed of the device.")] public float RechargeSpeed { get { return rechargeSpeed; } @@ -223,7 +221,6 @@ namespace Barotrauma.Items.Components } rechargeVoltage = 0.0f; - outputVoltage = 0.0f; } public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) @@ -241,7 +238,10 @@ namespace Barotrauma.Items.Components #endif RechargeSpeed = maxRechargeSpeed * aiRechargeTargetRatio; #if CLIENT - rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); + if (rechargeSpeedSlider != null) + { + rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); + } #endif character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" }, @@ -258,7 +258,10 @@ namespace Barotrauma.Items.Components #endif RechargeSpeed = 0.0f; #if CLIENT - rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); + if (rechargeSpeedSlider != null) + { + rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); + } #endif character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" }, new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, @@ -280,7 +283,10 @@ namespace Barotrauma.Items.Components float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f); RechargeSpeed = rechargeRate * MaxRechargeSpeed; #if CLIENT - rechargeSpeedSlider.BarScroll = rechargeRate; + if (rechargeSpeedSlider != null) + { + rechargeSpeedSlider.BarScroll = rechargeRate; + } #endif } } @@ -290,10 +296,6 @@ namespace Barotrauma.Items.Components { rechargeVoltage = Math.Min(power, 1.0f); } - else - { - outputVoltage = power; - } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs index 90dfc7663..8713a7ef6 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs @@ -41,30 +41,30 @@ namespace Barotrauma.Items.Components get { return powerLoad; } } - [Serialize(true, true), Editable(ToolTip = "Can the item be damaged if too much power is supplied to the power grid.")] + [Editable, Serialize(true, true, description: "Can the item be damaged if too much power is supplied to the power grid.")] public bool CanBeOverloaded { get; set; } - [Serialize(2.0f, true), Editable(MinValueFloat = 1.0f, ToolTip = + [Editable(MinValueFloat = 1.0f), Serialize(2.0f, true, 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.")] + + "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 { get; set; } - [Serialize(0.15f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, ToolTip = "The probability for a fire to start when the item breaks.")] + [Serialize(0.15f, true, 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)] + [Serialize(false, false, 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; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs index 60b0e2748..fb6627510 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs @@ -22,8 +22,8 @@ namespace Barotrauma.Items.Components //the maximum amount of power the item can draw from connected items protected float powerConsumption; - [Serialize(0.5f, true), Editable(ToolTip = "The minimum voltage required for the device to function. "+ - "The voltage is calculated as power / powerconsumption, meaning that a device "+ + [Editable, Serialize(0.5f, true, 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 { @@ -31,14 +31,14 @@ namespace Barotrauma.Items.Components set { minVoltage = value; } } - [Editable(ToolTip = "How much power the device draws (or attempts to draw) from the electrical grid."), Serialize(0.0f, true)] + [Editable, Serialize(0.0f, true, 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)] + [Serialize(false, true, description: "Is the device currently active. Inactive devices don't consume power.")] public override bool IsActive { get { return base.IsActive; } @@ -52,21 +52,21 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true)] + [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).")] public float CurrPowerConsumption { get {return currPowerConsumption; } set { currPowerConsumption = value; } } - [Serialize(0.0f, true)] + [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).")] public float Voltage { get { return voltage; } set { voltage = Math.Max(0.0f, value); } } - [Editable(ToolTip = "Can the item be damaged by electomagnetic pulses."), Serialize(true, true)] + [Editable, Serialize(true, true, description: "Can the item be damaged by electomagnetic pulses.")] public bool VulnerableToEMP { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs index 3699aa7bd..ec2827d76 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs @@ -57,14 +57,14 @@ namespace Barotrauma.Items.Components private float persistentStickJointTimer; - [Serialize(10.0f, false)] + [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.")] public float LaunchImpulse { get { return launchImpulse; } set { launchImpulse = value; } } - [Serialize(0.0f, false)] + [Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] public float LaunchRotation { get { return MathHelper.ToDegrees(LaunchRotationRadians); } @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components private set; } - [Serialize(false, false)] + [Serialize(false, false, description: "When set to true, the item can stick to any target it hits.")] //backwards compatibility, can stick to anything public bool DoesStick { @@ -85,49 +85,52 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { get; set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item stick to the structure it hits.")] public bool StickToStructures { get; set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Can the item stick to the item it hits.")] public bool StickToItems { get; set; } - [Serialize(false, false)] + [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.")] public bool Hitscan { get; set; } - [Serialize(1, false)] + [Serialize(1, false, 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 { get; set; } - [Serialize(false, false)] + [Serialize(false, false, description: "Should the item be deleted when it hits something.")] public bool RemoveOnHit { get; set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, false, description: "Random spread applied to the launch angle of the projectile (in degrees).")] public float Spread { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs index bacbdc216..bc3b1c9b9 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs @@ -21,64 +21,63 @@ namespace Barotrauma.Items.Components public float LastActiveTime; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "How fast the condition of the item deteriorates per second.")] + [Serialize(0.0f, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2, ToolTip = "Minimum initial delay before the item starts to deteriorate.")] + [Serialize(0.0f, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2, ToolTip = "Maximum initial delay before the item starts to deteriorate.")] + [Serialize(0.0f, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "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.")] + [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)] public float MinDeteriorationCondition { get; set; } - [Serialize(0f, true)] + [Serialize(0f, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The condition of the item has to be below this before the repair UI becomes usable. Percentages of max condition.")] + [Serialize(80.0f, true, description: "The condition of the item has to be below this before the repair UI becomes usable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float ShowRepairUIThreshold { get; set; } - [Serialize(100.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The amount of time it takes to fix the item with insufficient skill levels.")] + [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)] public float FixDurationLowSkill { get; set; } - [Serialize(10.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The amount of time it takes to fix the item with sufficient skill levels.")] + [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)] public float FixDurationHighSkill { get; set; } - //if enabled, the deterioration timer will always run regardless if the item is being used or not - [Serialize(false, false)] + [Serialize(false, false, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")] public bool DeteriorateAlways { get; @@ -199,7 +198,7 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - deteriorationTimer -= deltaTime; + deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); #if SERVER if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } #endif @@ -336,7 +335,7 @@ namespace Barotrauma.Items.Components else if (ic is Pump pump) { //pumps don't deteriorate if they're not running - if (Math.Abs(pump.FlowPercentage) > 1.0f) { return true; } + if (Math.Abs(pump.FlowPercentage) > 1.0f && pump.IsActive) { return true; } } else if (ic is Reactor reactor) { @@ -357,6 +356,26 @@ namespace Barotrauma.Items.Components return DeteriorateAlways; } + private float GetDeteriorationDelayMultiplier() + { + foreach (ItemComponent ic in item.Components) + { + if (ic is Engine engine) + { + return Math.Abs(engine.Force) / 100.0f; + } + else if (ic is Pump pump) + { + return Math.Abs(pump.FlowPercentage) / 100.0f; + } + else if (ic is Reactor reactor) + { + return (reactor.FissionRate + reactor.TurbineOutput) / 200.0f; + } + } + return 1.0f; + } + private void UpdateFixAnimation(Character character) { character.AnimController.UpdateUseItem(false, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((item.Condition / item.MaxCondition) % 0.1f)); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Rope.cs index 4b73bf17e..efce3fa72 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Rope.cs @@ -173,7 +173,7 @@ namespace Barotrauma.Items.Components if (i == ropeBodies.Length - 2) { - item.Combine(projectile); + item.Combine(projectile, user: null); ropeBodies[ropeBodies.Length - 1].Enabled = false; IsActive = false; } @@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components { //attempt to recontain the projectile in the launcher //eq automatically reload a spear into a speargun when picking the spear up - if (!projectile.body.Enabled) item.Combine(projectile); + if (!projectile.body.Enabled) item.Combine(projectile, user: null); foreach (PhysicsBody b in ropeBodies) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AdderComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AdderComponent.cs index bd7133478..1f6dad256 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AdderComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AdderComponent.cs @@ -11,25 +11,29 @@ namespace Barotrauma.Items.Components protected float[] timeSinceReceived; protected float[] receivedSignal; - + //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f), Serialize(999999.0f, true)] + [Serialize(999999.0f, true, description: "The output of the item is restricted below this value."), + InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMax { get; set; } - [InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f), Serialize(-999999.0f, true)] + [Serialize(-999999.0f, true, description: "The output of the item is restricted above this value."), + InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMin { get; set; } - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true)] + [InGameEditable(DecimalCount = 2), + Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the sum of the signals." + + " If set to 0, the inputs must be received at the same time.")] public float TimeFrame { get { return timeFrame; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AndComponent.cs index 52ec43086..550fce815 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AndComponent.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true)] + [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.")] public float TimeFrame { get { return timeFrame; } @@ -23,14 +23,14 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true)] + [InGameEditable, Serialize("1", true, description: "The signal sent when both inputs have received a non-zero signal.")] public string Output { get { return output; } set { output = value; } } - [InGameEditable, Serialize("", true)] + [InGameEditable, Serialize("", true, description: "The signal sent when both inputs have not received a non-zero signal (if empty, no signal is sent).")] public string FalseOutput { get { return falseOutput; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs index 0572819bc..686974715 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private List disconnectedWireIds; - [Serialize(false, true), Editable(ToolTip = "Locked connection panels cannot be rewired in-game.")] + [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.")] public bool Locked { get; @@ -171,9 +171,9 @@ namespace Barotrauma.Items.Components return true; } - public override void Load(XElement element) + public override void Load(XElement element, bool usePrefabValues) { - base.Load(element); + base.Load(element, usePrefabValues); List loadedConnections = new List(); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs index 5cd9ab35b..b98bd5348 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs @@ -12,9 +12,9 @@ namespace Barotrauma.Items.Components public bool ContinuousSignal; public bool State; public string Connection; - [Serialize("", false, translationTextTag = "Label.")] + [Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } - [Serialize("1", false)] + [Serialize("1", false, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable] public string Signal { get; set; } public string Name => "CustomInterfaceElement"; @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } private string[] labels; - [Serialize("", true)] + [Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.")] public string Labels { get { return string.Join(",", labels); } @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components } } private string[] signals; - [Serialize("", true)] + [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.")] public string Signals { //use semicolon as a separator because comma may be needed in the signals (for color or vector values for example) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs index e16eae435..1078353f8 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs @@ -9,9 +9,12 @@ namespace Barotrauma.Items.Components { public readonly string Signal; public readonly float SignalStrength; - public float SendTimer; + //in number of frames + public int SendTimer; + //in number of frames + public int SendDuration; - public DelayedSignal(string signal, float signalStrength, float sendTimer) + public DelayedSignal(string signal, float signalStrength, int sendTimer) { Signal = signal; SignalStrength = signalStrength; @@ -19,25 +22,35 @@ namespace Barotrauma.Items.Components } } - const int SignalQueueSize = 500; + private int signalQueueSize; + private int delayTicks; private Queue signalQueue; + + private DelayedSignal prevQueuedSignal; - [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true)] + 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).")] public float Delay { - get; - set; + get { return delay; } + set + { + if (value == delay) { return; } + delay = value; + delayTicks = (int)(delay / Timing.Step); + signalQueueSize = delayTicks * 2; + } } - [InGameEditable(ToolTip = "Should the component discard previously received signals when a new one is received."), Serialize(false, true)] + [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.")] public bool ResetWhenSignalReceived { get; set; } - [InGameEditable(ToolTip = "Should the component discard previously received signals when the incoming signal changes."), Serialize(false, true)] + [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.")] public bool ResetWhenDifferentSignalReceived { get; @@ -55,13 +68,15 @@ namespace Barotrauma.Items.Components { foreach (var val in signalQueue) { - val.SendTimer -= deltaTime; + val.SendTimer -= 1; } - while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0.0f) + while (signalQueue.Count > 0 && signalQueue.Peek().SendTimer <= 0) { - var signalOut = signalQueue.Dequeue(); + var signalOut = signalQueue.Peek(); + signalOut.SendDuration -= 1; item.SendSignal(0, signalOut.Signal, "signal_out", null, signalStrength: signalOut.SignalStrength); + if (signalOut.SendDuration <= 0) { signalQueue.Dequeue(); } else { break; } } } @@ -70,13 +85,28 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "signal_in": - if (signalQueue.Count >= SignalQueueSize) return; - if (ResetWhenSignalReceived) signalQueue.Clear(); + if (signalQueue.Count >= signalQueueSize) { return; } + if (ResetWhenSignalReceived) { prevQueuedSignal = null; signalQueue.Clear(); } if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal != signal) { + prevQueuedSignal = null; signalQueue.Clear(); } - signalQueue.Enqueue(new DelayedSignal(signal, signalStrength, Delay)); + + if (prevQueuedSignal != null && + prevQueuedSignal.Signal == signal && + MathUtils.NearlyEqual(prevQueuedSignal.SignalStrength, signalStrength) && + ((prevQueuedSignal.SendTimer + prevQueuedSignal.SendDuration == delayTicks) || (prevQueuedSignal.SendTimer <= 0 && prevQueuedSignal.SendDuration > 0))) + { + prevQueuedSignal.SendDuration += 1; + return; + } + + prevQueuedSignal = new DelayedSignal(signal, signalStrength, delayTicks) + { + SendDuration = 1 + }; + signalQueue.Enqueue(prevQueuedSignal); break; } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/EqualsComponent.cs index a406d0609..467418095 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/EqualsComponent.cs @@ -15,21 +15,21 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable, Serialize("1", true)] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signals are equal.")] public string Output { get { return output; } set { output = value; } } - [InGameEditable, Serialize("", true)] + [InGameEditable, Serialize("", true, description: "The signal this item outputs when the received signals are not equal.")] public string FalseOutput { get { return falseOutput; } set { falseOutput = value; } } - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true)] + [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.")] public float TimeFrame { get { return timeFrame; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs index 0cd9b4373..3aa438c7e 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs @@ -25,7 +25,8 @@ namespace Barotrauma.Items.Components public PhysicsBody ParentBody; - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f), Serialize(100.0f, true)] + [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive."), + Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { get { return range; } @@ -40,8 +41,8 @@ namespace Barotrauma.Items.Components public float Rotation; - [Editable(ToolTip = "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."), Serialize(true, true)] + [Editable, Serialize(true, true, 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.")] public bool CastShadows { get { return castShadows; } @@ -54,8 +55,8 @@ namespace Barotrauma.Items.Components } } - [Editable(ToolTip = "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."), Serialize(false, true)] + [Editable, Serialize(false, true, 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.")] public bool DrawBehindSubs { get { return drawBehindSubs; } @@ -68,7 +69,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, true, description: "Is the light currently on.")] public bool IsOn { get { return IsActive; } @@ -83,7 +84,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, false)] + [Serialize(0.0f, false, 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; } @@ -93,7 +94,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(0.0f, true)] + [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] public float BlinkFrequency { get { return blinkFrequency; } @@ -103,7 +104,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1.0,1.0,1.0,1.0", true)] + [InGameEditable, Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).")] public Color LightColor { get { return lightColor; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MemoryComponent.cs index 4113a5d88..ebfd0b2c4 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MemoryComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class MemoryComponent : ItemComponent { - [InGameEditable, Serialize("", true)] + [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.")] public string Value { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs index 87a8fa1e3..175bd4399 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs @@ -9,32 +9,23 @@ namespace Barotrauma.Items.Components partial class MotionSensor : ItemComponent { private const float UpdateInterval = 0.1f; - - private string output, falseOutput; - - private bool motionDetected; - private float rangeX, rangeY; private Vector2 detectOffset; private float updateTimer; - [Serialize(false, false)] - public bool MotionDetected - { - get { return motionDetected; } - set { motionDetected = value; } - } + [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).")] + public bool MotionDetected { get; set; } - [Serialize(false, true), Editable] + [Editable, Serialize(false, true, description: "Should the sensor only detect the movement of humans?")] public bool OnlyHumans { get; set; } - [InGameEditable, Serialize(0.0f, true)] + [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.")] public float RangeX { get { return rangeX; } @@ -43,7 +34,7 @@ namespace Barotrauma.Items.Components rangeX = MathHelper.Clamp(value, 0.0f, 1000.0f); } } - [InGameEditable, Serialize(0.0f, true)] + [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.")] public float RangeY { get { return rangeY; } @@ -53,7 +44,7 @@ namespace Barotrauma.Items.Components } } - [Serialize("0,0", true), Editable(ToolTip = "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")] + [Editable, 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.")] public Vector2 DetectOffset { get { return detectOffset; } @@ -65,21 +56,13 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true)] - public string Output - { - get { return output; } - set { output = value; } - } + [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.")] + public string Output { get; set; } - [InGameEditable, Serialize("", true)] - public string FalseOutput - { - get { return falseOutput; } - set { falseOutput = value; } - } + [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.")] + public string FalseOutput { get; set; } - [Editable(ToolTip = "How fast the objects within the detector's range have to be moving (in m/s).", DecimalCount = 3), Serialize(0.01f, true)] + [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).")] public float MinimumVelocity { get; @@ -88,7 +71,7 @@ namespace Barotrauma.Items.Components public MotionSensor(Item item, XElement element) - : base (item, element) + : base(item, element) { IsActive = true; @@ -101,21 +84,21 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - string signalOut = motionDetected ? output : falseOutput; + string signalOut = MotionDetected ? Output : FalseOutput; if (!string.IsNullOrEmpty(signalOut)) item.SendSignal(1, signalOut, "state_out", null); updateTimer -= deltaTime; if (updateTimer > 0.0f) return; - motionDetected = false; + MotionDetected = false; updateTimer = UpdateInterval; if (item.body != null && item.body.Enabled) { if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity) { - motionDetected = true; + MotionDetected = true; } } @@ -126,7 +109,7 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { - if (OnlyHumans && c.ConfigPath != Character.HumanConfigFile) { continue; } + if (OnlyHumans && !c.IsHuman) { continue; } //do a rough check based on the position of the character's collider first //before the more accurate limb-based check @@ -140,11 +123,20 @@ namespace Barotrauma.Items.Components if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) continue; if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { - motionDetected = true; + MotionDetected = true; break; } } } } + + public override void FlipX(bool relativeToSub) + { + detectOffset.X = -detectOffset.X; + } + public override void FlipY(bool relativeToSub) + { + detectOffset.Y = -detectOffset.Y; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/OscillatorComponent.cs index 841c08cd6..3b2b7ece7 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/OscillatorComponent.cs @@ -20,14 +20,17 @@ namespace Barotrauma.Items.Components private float phase; - [InGameEditable, Serialize(WaveType.Pulse, true)] + [InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." + + " Pulse: periodically sends out a signal of 1." + + " Sine: sends out a sine wave oscillating between -1 and 1." + + " Square: sends out a signal that alternates between 0 and 1.")] public WaveType OutputType { get; set; } - [InGameEditable(DecimalCount = 2), Serialize(1.0f, true)] + [InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).")] public float Frequency { get { return frequency; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RegExFindComponent.cs index 5a91b2f5e..e40ca77ed 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RegExFindComponent.cs @@ -16,16 +16,16 @@ namespace Barotrauma.Items.Components private bool nonContinuousOutputSent; - [InGameEditable, Serialize("1", true)] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.")] public string Output { get; set; } - [InGameEditable, Serialize("0", true)] + [Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.")] public string FalseOutput { get; set; } - [Serialize(true, true), InGameEditable(ToolTip = "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.")] + [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.")] public bool ContinuousOutput { get; set; } - [InGameEditable, Serialize("", true)] + [InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.")] public string Expression { get { return expression; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs index e9db3ebc7..7fe73ed1b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components { "signal_in5", "signal_out5" } }; - [Editable, Serialize(1000.0f, true)] + [Editable, Serialize(1000.0f, true, description: "The maximum amount of power that can pass through the item.")] public float MaxPower { get { return maxPower; } @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.")] public bool IsOn { get diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SignalCheckComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SignalCheckComponent.cs index 0e1cae0a1..13e2acc3c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SignalCheckComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SignalCheckComponent.cs @@ -4,29 +4,13 @@ namespace Barotrauma.Items.Components { class SignalCheckComponent : ItemComponent { - private string output, falseOutput; + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.")] + public string Output { get; set; } + [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.")] + public string FalseOutput { get; set; } - private string targetSignal; - - [InGameEditable, Serialize("1", true)] - public string Output - { - get { return output; } - set { output = value; } - } - [InGameEditable, Serialize("0", true)] - public string FalseOutput - { - get { return falseOutput; } - set { falseOutput = value; } - } - - [InGameEditable, Serialize("", true)] - public string TargetSignal - { - get { return targetSignal; } - set { targetSignal = value; } - } + [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.")] + public string TargetSignal { get; set; } public SignalCheckComponent(Item item, XElement element) : base(item, element) @@ -38,17 +22,17 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "signal_in": - string signalOut = (signal == targetSignal) ? output : falseOutput; + string signalOut = (signal == TargetSignal) ? Output : FalseOutput; if (string.IsNullOrWhiteSpace(signalOut)) return; item.SendSignal(stepsTaken, signalOut, "signal_out", sender, signalStrength); break; case "set_output": - output = signal; + Output = signal; break; case "set_targetsignal": - targetSignal = signal; + TargetSignal = signal; break; } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SmokeDetector.cs index 8eeba6f8a..feed5a033 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/SmokeDetector.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components { class SmokeDetector : ItemComponent { - [Serialize(50.0f, false)] + [Serialize(50.0f, false, description: "How large the fire has to be for the detector to react to it.")] public float FireSizeThreshold { get; set; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WaterDetector.cs index 7677141bd..d5ad89235 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WaterDetector.cs @@ -4,27 +4,17 @@ namespace Barotrauma.Items.Components { class WaterDetector : ItemComponent { - private string output, falseOutput; - //how often the detector can switch from state to another const float StateSwitchInterval = 1.0f; private bool isInWater; private float stateSwitchDelay; - [InGameEditable, Serialize("1", true)] - public string Output - { - get { return output; } - set { output = value; } - } + [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.")] + public string Output { get; set; } - [InGameEditable, Serialize("0", true)] - public string FalseOutput - { - get { return falseOutput; } - set { falseOutput = value; } - } + [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.")] + public string FalseOutput { get; set; } public WaterDetector(Item item, XElement element) : base(item, element) @@ -64,7 +54,7 @@ namespace Barotrauma.Items.Components } } - string signalOut = isInWater ? output : falseOutput; + string signalOut = isInWater ? Output : FalseOutput; if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(0, signalOut, "signal_out", null); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs index addb59590..b14ec6b00 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs @@ -19,17 +19,17 @@ namespace Barotrauma.Items.Components private string prevSignal; - [Serialize(Character.TeamType.None, false)] + [Serialize(Character.TeamType.None, false, description: "WiFi components can only communicate with components that have the same Team ID.")] public Character.TeamType TeamID { get; set; } - [Serialize(20000.0f, false)] + [Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")] public float Range { get { return range; } set { range = Math.Max(value, 0.0f); } } - [InGameEditable, Serialize(1, true)] + [InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.")] public int Channel { get { return channel; } @@ -39,25 +39,24 @@ namespace Barotrauma.Items.Components } } - [Editable(ToolTip = - "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."), Serialize(false, false)] + [Editable, Serialize(false, false, 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.")] public bool LinkToChat { get; set; } - [Editable(ToolTip = "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."), Serialize(1.0f, true)] + [Editable, Serialize(1.0f, true, 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 { get; set; } - [Editable(ToolTip = "If set to true, the component will only create chat messages when the received signal changes."), Serialize(false, true)] + [Editable, Serialize(false, true, description: "If set to true, the component will only create chat messages when the received signal changes.")] public bool DiscardDuplicateChatMessages { get; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs index 3ad02ff77..a2e0ae132 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs @@ -55,6 +55,8 @@ namespace Barotrauma.Items.Components public bool Hidden; + private float removeNodeDelay; + private bool locked; public bool Locked { @@ -71,7 +73,7 @@ namespace Barotrauma.Items.Components get { return connections; } } - [Serialize(5000.0f, false)] + [Serialize(5000.0f, false, description: "The maximum distance the wire can extend (in pixels).")] public float MaxLength { get; @@ -255,17 +257,18 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { - ClearConnections(dropper); + ClearConnections(dropper); IsActive = false; } public override void Update(float deltaTime, Camera cam) { - if (nodes.Count == 0) return; + removeNodeDelay -= deltaTime; + if (nodes.Count == 0) { return; } Submarine sub = null; - if (connections[0] != null && connections[0].Item.Submarine != null) sub = connections[0].Item.Submarine; - if (connections[1] != null && connections[1].Item.Submarine != null) sub = connections[1].Item.Submarine; + if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; } + if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; } if (Screen.Selected != GameMain.SubEditorScreen) { @@ -354,10 +357,12 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (character == null) return false; -#if CLIENT - if (character == Character.Controlled && character.SelectedConstruction != null) return false; -#endif + if (character == null) { return false; } + if (character == Character.Controlled && character.SelectedConstruction != null) { return false; } + if (Screen.Selected == GameMain.SubEditorScreen && !PlayerInput.LeftButtonClicked()) + { + return false; + } if (newNodePos != Vector2.Zero && canPlaceNode && nodes.Count > 0 && Vector2.Distance(newNodePos, nodes[nodes.Count - 1]) > nodeDistance) { @@ -384,11 +389,12 @@ namespace Barotrauma.Items.Components public override bool SecondaryUse(float deltaTime, Character character = null) { - if (nodes.Count > 1) + if (nodes.Count > 1 && removeNodeDelay <= 0.0f) { nodes.RemoveAt(nodes.Count - 1); UpdateSections(); } + removeNodeDelay = 0.1f; Drawable = IsActive || sections.Count > 0; return true; @@ -668,9 +674,9 @@ namespace Barotrauma.Items.Components UpdateSections(); } - public override void Load(XElement componentElement) + public override void Load(XElement componentElement, bool usePrefabValues) { - base.Load(componentElement); + base.Load(componentElement, usePrefabValues); string nodeString = componentElement.GetAttributeString("nodes", ""); if (nodeString == "") return; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs index 1d4fb631f..8cafa873f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components private Character user; - [Serialize("0,0", false)] + [Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] public Vector2 BarrelPos { get @@ -57,21 +57,21 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, false)] + [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).")] public float LaunchImpulse { get { return launchImpulse; } set { launchImpulse = value; } } - [Serialize(5.0f, false), Editable(0.0f, 1000.0f)] + [Editable(0.0f, 1000.0f), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")] public float Reload { get { return reloadTime; } set { reloadTime = value; } } - [Serialize("0.0,0.0", true), Editable] + [Editable, Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate. TODO")] public Vector2 RotationLimits { get @@ -94,39 +94,49 @@ namespace Barotrauma.Items.Components } } - [Serialize(5.0f, false), Editable(0.0f, 1000.0f, DecimalCount = 2)] + [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" + + " with insufficient skills to operate it. Higher values make the barrel rotate faster.")] public float SpringStiffnessLowSkill { get; private set; } - [Serialize(2.0f, false), Editable(0.0f, 1000.0f, DecimalCount = 2)] + [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" + + " with sufficient skills to operate it. Higher values make the barrel rotate faster.")] public float SpringStiffnessHighSkill { get; private set; } - [Serialize(50.0f, false), Editable(0.0f, 1000.0f, DecimalCount = 2)] + [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" + + " 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 { get; private set; } - [Serialize(10.0f, false), Editable(0.0f, 1000.0f, DecimalCount = 2)] + [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" + + " 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 { get; private set; } - [Serialize(1.0f, false), Editable(0.0f, 100.0f, DecimalCount = 2)] + [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.")] public float RotationSpeedLowSkill { get; private set; } - [Serialize(5.0f, false), Editable(0.0f, 100.0f, DecimalCount = 2)] + [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."),] public float RotationSpeedHighSkill { get; @@ -134,7 +144,7 @@ namespace Barotrauma.Items.Components } private float baseRotationRad; - [Serialize(0.0f, true), Editable(0.0f, 360.0f)] + [Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.")] public float BaseRotation { get { return MathHelper.ToDegrees(baseRotationRad); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs index de3eacc57..469a42f5c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs @@ -104,7 +104,7 @@ namespace Barotrauma case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; - HideLimb = false; + HideLimb = type == WearableType.Husk || type == WearableType.Herpes; HideOtherWearables = false; InheritLimbDepth = true; InheritTextureScale = true; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs index 10343a8c8..692a9e94c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs @@ -11,7 +11,7 @@ namespace Barotrauma { public readonly Entity Owner; - protected int capacity; + protected readonly int capacity; public Item[] Items; protected bool[] hideEmptySlot; @@ -25,7 +25,7 @@ namespace Barotrauma get { return capacity; } } - public Inventory(Entity owner, int capacity, Vector2? centerPos = null, int slotsPerRow = 5) + public Inventory(Entity owner, int capacity, int slotsPerRow = 5) { this.capacity = capacity; @@ -132,7 +132,7 @@ namespace Barotrauma //there's already an item in the slot if (Items[i] != null && allowCombine) { - if (Items[i].Combine(item)) + if (Items[i].Combine(item, user)) { System.Diagnostics.Debug.Assert(Items[i] != null); return true; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Item.cs b/Barotrauma/BarotraumaShared/Source/Items/Item.cs index fd3bbc5a2..f45ee95c7 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Item.cs @@ -254,7 +254,7 @@ namespace Barotrauma protected set; } - [Serialize("1.0,1.0,1.0,1.0", true), Editable(ToolTip = "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", 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.")] public Color ContainerColor { get; @@ -505,7 +505,7 @@ namespace Barotrauma get { return ownInventory; } } - [Serialize(false, true), Editable(ToolTip = + [Editable, Serialize(false, true, 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; } @@ -611,8 +611,6 @@ namespace Barotrauma break; case "aitarget": aiTarget = new AITarget(this, subElement); - aiTarget.SoundRange = aiTarget.MinSoundRange; - aiTarget.SightRange = aiTarget.MinSightRange; break; default: ItemComponent ic = ItemComponent.Load(subElement, this, itemPrefab.ConfigFile); @@ -1157,11 +1155,9 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { base.Update(deltaTime, cam); - //aitarget goes silent/invisible if the components don't keep it active if (aiTarget != null) { - aiTarget.SightRange -= deltaTime * (aiTarget.MaxSightRange / aiTarget.FadeOutTime); - aiTarget.SoundRange -= deltaTime * (aiTarget.MaxSoundRange / aiTarget.FadeOutTime); + aiTarget.Update(deltaTime); } bool broken = condition <= 0.0f; @@ -1794,13 +1790,13 @@ namespace Barotrauma if (remove) { Spawner?.AddToRemoveQueue(this); } } - public bool Combine(Item item) + public bool Combine(Item item, Character user) { if (item == this) { return false; } bool isCombined = false; foreach (ItemComponent ic in components) { - if (ic.Combine(item)) { isCombined = true; } + if (ic.Combine(item, user)) { isCombined = true; } } #if CLIENT if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Combine, item.ID }); } @@ -1976,7 +1972,7 @@ namespace Barotrauma SerializableProperty property = allProperties[propertyIndex].Second; if (inGameEditableOnly && parentObject is ItemComponent ic) { - if (!ic.AllowInGameEditing) allowEditing = false; + if (!ic.AllowInGameEditing) { allowEditing = false; } } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !CanClientAccess(sender)) @@ -2138,18 +2134,27 @@ namespace Barotrauma } } + bool thisIsOverride = element.GetAttributeBool("isoverride", false); + + //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; List unloadedComponents = new List(item.components); foreach (XElement subElement in element.Elements()) { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); if (component == null) { continue; } - - component.Load(subElement); + component.Load(subElement, usePrefabValues); unloadedComponents.Remove(component); } + if (usePrefabValues) + { + //use prefab scale when overriding a non-overridden item or vice versa + item.Scale = prefab.ConfigElement.GetAttributeFloat(item.scale, "scale", "Scale"); + } - if (element.GetAttributeBool("flippedx", false)) item.FlipX(false); - if (element.GetAttributeBool("flippedy", false)) item.FlipY(false); + if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); } + if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); } float condition = element.GetAttributeFloat("condition", item.MaxCondition); item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition); @@ -2179,8 +2184,9 @@ namespace Barotrauma new XAttribute("identifier", Prefab.Identifier), new XAttribute("ID", ID)); - if (FlippedX) element.Add(new XAttribute("flippedx", true)); - if (FlippedY) element.Add(new XAttribute("flippedy", true)); + if (Prefab.IsOverride) { element.Add(new XAttribute("isoverride", "true")); } + if (FlippedX) { element.Add(new XAttribute("flippedx", true)); } + if (FlippedY) { element.Add(new XAttribute("flippedy", true)); } if (condition < Prefab.Health) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs index 8fd102884..793062786 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs @@ -14,8 +14,8 @@ namespace Barotrauma get { return container; } } - public ItemInventory(Item owner, ItemContainer container, int capacity, Vector2? centerPos = null, int slotsPerRow = 5) - : base(owner, capacity, centerPos, slotsPerRow) + public ItemInventory(Item owner, ItemContainer container, int capacity, int slotsPerRow = 5) + : base(owner, capacity, slotsPerRow) { this.container = container; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs index 2ff9aaae1..e5d3dc565 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs @@ -150,6 +150,11 @@ namespace Barotrauma /// public readonly string OriginalName; + /// + /// Is this prefab overriding a prefab in another content package + /// + public bool IsOverride; + public string ConfigFile { get { return configFile; } @@ -412,22 +417,58 @@ namespace Barotrauma } XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc?.Root == null) - { - DebugConsole.ThrowError("File \"" + filePath + "\" could not be loaded."); - continue; - } + if (doc == null) { return; } - if (doc.Root.Name.ToString().ToLowerInvariant() == "items") + var rootElement = doc.Root; + switch (rootElement.Name.ToString().ToLowerInvariant()) { - foreach (XElement element in doc.Root.Elements()) - { - new ItemPrefab(element, filePath); - } - } - else - { - new ItemPrefab(doc.Root, filePath); + case "item": + new ItemPrefab(rootElement, filePath, false); + break; + case "items": + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + var itemElement = element.GetChildElement("item"); + if (itemElement != null) + { + new ItemPrefab(itemElement, filePath, true) + { + IsOverride = true + }; + } + else + { + DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {filePath}"); + } + } + else + { + new ItemPrefab(element, filePath, false); + } + } + break; + case "override": + var items = rootElement.GetChildElement("items"); + if (items != null) + { + foreach (var element in items.Elements()) + { + new ItemPrefab(element, filePath, true) + { + IsOverride = true + }; + } + } + foreach (var element in rootElement.GetChildElements("item")) + { + new ItemPrefab(element, filePath, true); + } + break; + default: + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {filePath}"); + break; } } @@ -444,7 +485,7 @@ namespace Barotrauma } } - public ItemPrefab(XElement element, string filePath) + public ItemPrefab(XElement element, string filePath, bool allowOverriding) { configFile = filePath; ConfigElement = element; @@ -702,19 +743,13 @@ namespace Barotrauma 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."); } - if (!string.IsNullOrEmpty(identifier)) - { - MapEntityPrefab existingPrefab = List.Find(e => e.Identifier == identifier); - if (existingPrefab != null) - { - DebugConsole.ThrowError( - "Map entity prefabs \"" + name + "\" and \"" + existingPrefab.Name + "\" have the same identifier!"); - } - } AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList(); - List.Add(this); + if (HandleExisting(identifier, allowOverriding, filePath)) + { + List.Add(this); + } } public PriceInfo GetPrice(Location location) diff --git a/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs index cf23f7d59..739fdf82b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs @@ -164,7 +164,7 @@ namespace Barotrauma if (element.Attribute("name") != null) { //backwards compatibility + a console warning - DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item identifiers or tags instead of names."); + 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]); //attempt to convert to identifiers and tags List convertedIdentifiers = new List(); @@ -184,14 +184,28 @@ namespace Barotrauma } else { - identifiers = element.GetAttributeStringArray("identifiers", new string[0]); - if (identifiers.Length == 0) identifiers = element.GetAttributeStringArray("identifier", new string[0]); + identifiers = element.GetAttributeStringArray("items", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("item", null, convertToLowerInvariant: true); + if (identifiers == null) + { + identifiers = element.GetAttributeStringArray("identifiers", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("tags", null, convertToLowerInvariant: true); + if (identifiers == null) + { + identifiers = element.GetAttributeStringArray("identifier", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("tag", new string[0], convertToLowerInvariant: true); + } + } } - string[] excludedIdentifiers = element.GetAttributeStringArray("excludedidentifiers", new string[0]); - if (excludedIdentifiers.Length == 0) excludedIdentifiers = element.GetAttributeStringArray("excludedidentifier", new string[0]); + string[] excludedIdentifiers = element.GetAttributeStringArray("excludeditems", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludeditem", null, convertToLowerInvariant: true); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeStringArray("excludedidentifiers", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludedtags", null, convertToLowerInvariant: true); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeStringArray("excludedidentifier", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludedtag", new string[0], convertToLowerInvariant: true); + } + } - if (identifiers.Length == 0 && excludedIdentifiers.Length == 0) return null; + if (identifiers.Length == 0 && excludedIdentifiers.Length == 0) { return null; } RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs index ce914981f..5b16fcfe7 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs @@ -209,7 +209,7 @@ namespace Barotrauma distFactors.Add(limb, distFactor); List modifiedAfflictions = new List(); - foreach (Affliction affliction in attack.Afflictions) + foreach (Affliction affliction in attack.Afflictions.Keys) { modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / c.AnimController.Limbs.Length)); } @@ -248,26 +248,20 @@ namespace Barotrauma Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - } - + } + //sever joints if (c.IsDead && attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { - if (!distFactors.ContainsKey(limb)) continue; - - foreach (LimbJoint joint in c.AnimController.LimbJoints) + if (!distFactors.ContainsKey(limb)) { continue; } + if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb]) { - if (joint.IsSevered || (joint.LimbA != limb && joint.LimbB != limb)) continue; - - if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb]) - { - c.AnimController.SeverLimbJoint(joint); - } + c.TrySeverLimbJoints(limb, 1.0f); } } - } + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/Source/Map/ItemAssemblyPrefab.cs index 002befbf7..4da4e4ff1 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/ItemAssemblyPrefab.cs @@ -25,8 +25,8 @@ namespace Barotrauma { configPath = filePath; XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) return; - + if (doc == null) { return; } + name = doc.Root.GetAttributeString("name", ""); identifier = doc.Root.GetAttributeString("identifier", null) ?? name.ToLowerInvariant().Replace(" ", ""); configElement = doc.Root; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelGenerationParams.cs index ef77cef3e..bccc75d3d 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelGenerationParams.cs @@ -13,7 +13,7 @@ namespace Barotrauma public readonly string Description; public readonly List AllowedZones = new List(); - + public Biome(string name, string description) { Name = name; @@ -55,7 +55,7 @@ namespace Barotrauma get; private set; } - + private int minWidth, maxWidth, height; private Point voronoiSiteInterval; @@ -86,7 +86,7 @@ namespace Barotrauma private float cellIrregularity; private int mountainCountMin, mountainCountMax; - + private int mountainHeightMin, mountainHeightMax; private int ruinCount; @@ -134,8 +134,8 @@ namespace Barotrauma get; set; } - - [Serialize(1000, true), Editable(MinValueInt = 0, MaxValueInt = 100000, ToolTip = "The total number of level objects (vegetation, vents, etc) in the level.")] + + [Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] public int LevelObjectAmount { get; @@ -162,9 +162,8 @@ namespace Barotrauma get { return height; } set { height = Math.Max(value, 2000); } } - - [Serialize("3000, 3000", true), Editable( - ToolTip = "How far from each other voronoi sites are placed. " + + + [Editable, Serialize("3000, 3000", true, 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 @@ -177,7 +176,7 @@ namespace Barotrauma } } - [Serialize("700,700", true), Editable(ToolTip = "How much random variation to apply to the positions of the voronoi sites on each axis. "+ + [Editable, Serialize("700,700", true, 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 { @@ -189,8 +188,8 @@ namespace Barotrauma MathHelper.Clamp(value.Y, 0, voronoiSiteInterval.Y)); } } - - [Serialize(1000, true), Editable(MinValueInt = 100, MaxValueInt = 10000, ToolTip = "The edges of the individual wall cells are subdivided into edges of this size. " + + [Editable(MinValueInt = 100, MaxValueInt = 10000), Serialize(1000, true, 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 @@ -203,8 +202,8 @@ namespace Barotrauma } - [Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, ToolTip = "How much the individual wall cells are rounded. " - +"Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")] + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, true, 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 { get { return cellRoundingAmount; } @@ -214,7 +213,7 @@ namespace Barotrauma } } - [Serialize(0.1f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, ToolTip = "How much random variance is applied to the edges of the cells. " + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, true, 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 { @@ -226,7 +225,7 @@ namespace Barotrauma } - [Serialize("5000, 10000", true), Editable(ToolTip = "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] + [Editable, 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.")] public Point MainPathNodeIntervalRange { get { return mainPathNodeIntervalRange; } @@ -237,14 +236,14 @@ namespace Barotrauma } } - [Serialize(5, true), Editable(ToolTip = "The number of small tunnels placed along the main path.")] + [Editable, Serialize(5, true, description: "The number of small tunnels placed along the main path.")] public int SmallTunnelCount { get { return smallTunnelCount; } set { smallTunnelCount = MathHelper.Clamp(value, 0, 100); } } - - [Serialize("5000, 10000", true), Editable(ToolTip = "The minimum and maximum length of small tunnels placed along the main path.")] + + [Editable, Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")] public Point SmallTunnelLengthRange { get { return smallTunnelLengthRange; } @@ -269,21 +268,21 @@ namespace Barotrauma set; } - [Serialize(300000, true), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f, ToolTip = "How far below the level the sea floor is placed.")] + [Serialize(300000, true, 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), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f, ToolTip = "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor.")] + [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)] public int SeaFloorVariance { get { return seaFloorVariance; } set { seaFloorVariance = value; } } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20, ToolTip = "The minimum number of mountains on the sea floor.")] + [Serialize(0, true, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MountainCountMin { get { return mountainCountMin; } @@ -293,7 +292,7 @@ namespace Barotrauma } } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20, ToolTip = "The maximum number of mountains on the sea floor.")] + [Serialize(0, true, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MountainCountMax { get { return mountainCountMax; } @@ -302,8 +301,8 @@ namespace Barotrauma mountainCountMax = Math.Max(value, 0); } } - - [Serialize(1000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000, ToolTip = "The minimum height of the mountains on the sea floor.")] + + [Serialize(1000, true, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int MountainHeightMin { get { return mountainHeightMin; } @@ -312,8 +311,8 @@ namespace Barotrauma mountainHeightMin = Math.Max(value, 0); } } - - [Serialize(5000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000, ToolTip = "The maximum height of the mountains on the sea floor.")] + + [Serialize(5000, true, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int MountainHeightMax { get { return mountainHeightMax; } @@ -323,21 +322,21 @@ namespace Barotrauma } } - [Serialize(1, true), Editable(MinValueInt = 0, MaxValueInt = 50, ToolTip = "The number of alien ruins in the level.")] + [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 50)] public int RuinCount { get { return ruinCount; } set { ruinCount = MathHelper.Clamp(value, 0, 10); } } - [Serialize(0.4f, true), Editable(ToolTip = "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.")] + [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()] public float BottomHoleProbability { get { return bottomHoleProbability; } set { bottomHoleProbability = MathHelper.Clamp(value, 0.0f, 1.0f); } } - [Serialize(1.0f, true), Editable(ToolTip = "Scale of the water particle texture.")] + [Serialize(1.0f, true, description: "Scale of the water particle texture."), Editable] public float WaterParticleScale { get { return waterParticleScale; } @@ -351,7 +350,7 @@ namespace Barotrauma public Sprite WallEdgeSprite { get; private set; } public Sprite WallEdgeSpriteSpecular { get; private set; } public Sprite WaterParticles { get; private set; } - + public static List GetBiomes() { return biomes; @@ -386,7 +385,7 @@ namespace Barotrauma { Name = element == null ? "default" : element.Name.ToString(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - + string biomeStr = element.GetAttributeString("biomes", ""); if (string.IsNullOrWhiteSpace(biomeStr)) { @@ -450,16 +449,29 @@ namespace Barotrauma { files = new List() { "Content/Map/LevelGenerationParameters.xml" }; } - + List biomeElements = new List(); List levelParamElements = new List(); foreach (string file in files) { XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null || doc.Root == null) return; + if (doc == null) { continue; } + var mainElement = doc.Root; + if (doc.Root.IsOverride()) + { + mainElement = doc.Root.FirstElement(); + biomeElements.Clear(); + levelParamElements.Clear(); + DebugConsole.NewMessage($"Overriding the level generation parameters with '{file}'", Color.Yellow); + } + else if (biomeElements.Any() || levelParamElements.Any()) + { + DebugConsole.ThrowError($"Error in '{file}': Another level generation parameter file already loaded! Use tags to override it."); + break; + } - foreach (XElement element in doc.Root.Elements()) + foreach (XElement element in mainElement.Elements()) { if (element.Name.ToString().ToLowerInvariant() == "biomes") { diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 04eda0a0a..aa1ab7153 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -78,7 +78,7 @@ namespace Barotrauma /// /// Which sides of a wall the object can appear on. /// - [Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), true), Editable(ToolTip = "Which sides of a wall the object can spawn on.")] + [Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), true, description: "Which sides of a wall the object can spawn on."), Editable] public Alignment Alignment { get; @@ -117,15 +117,15 @@ namespace Barotrauma private set; } - [Serialize("0.0,1.0", true), Editable()] + [Serialize("0.0,1.0", true), Editable] public Vector2 DepthRange { get; private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, - ToolTip = "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.")] + [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.")] /// /// 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. @@ -136,8 +136,8 @@ namespace Barotrauma private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, - ToolTip = "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " + + [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 " + " 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.")] /// @@ -152,15 +152,14 @@ namespace Barotrauma private set; } - [Serialize(false, true), Editable(ToolTip = "Should the object be rotated to align it with the wall surface it spawns on.")] + [Editable, Serialize(false, true, description: "Should the object be rotated to align it with the wall surface it spawns on.")] public bool AlignWithSurface { get; private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, - ToolTip = "Minimum length of a graph edge the object can spawn on.")] + [Serialize(0.0f, true, 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. /// @@ -171,7 +170,7 @@ namespace Barotrauma } private Vector2 randomRotation; - [Serialize("0.0,0.0", true), Editable(ToolTip = "How much the rotation of the object can vary (min and max values in degrees).")] + [Editable, Serialize("0.0,0.0", true, 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)); } @@ -184,7 +183,7 @@ namespace Barotrauma public Vector2 RandomRotationRad => randomRotation; private float swingAmount; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f, ToolTip = "How much the object swings (in degrees).")] + [Serialize(0.0f, true, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)] public float SwingAmount { get { return MathHelper.ToDegrees(swingAmount); } @@ -196,30 +195,30 @@ namespace Barotrauma public float SwingAmountRad => swingAmount; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, ToolTip = "How fast the object swings.")] + [Serialize(0.0f, true, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SwingFrequency { get; private set; } - [Serialize("0.0,0.0", true), Editable(ToolTip = "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", 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%.")] public Vector2 ScaleOscillation { get; private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, ToolTip = "How fast the object's scale oscillates.")] + [Serialize(0.0f, true, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float ScaleOscillationFrequency { get; private set; } - [Serialize(1.0f, true), Editable(ToolTip = "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. "+ + [Editable, Serialize(1.0f, true, 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.")] public float Commonness { @@ -227,7 +226,7 @@ namespace Barotrauma private set; } - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, ToolTip = "How much the object disrupts submarine's sonar.")] + [Serialize(0.0f, true, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SonarDisruption { get; @@ -287,9 +286,19 @@ namespace Barotrauma try { XDocument doc = XMLExtensions.TryLoadXml(configPath); - if (doc == null || doc.Root == null) return; - - foreach (XElement element in doc.Root.Elements()) + 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.NewMessage($"Loading additional level object prefabs from file '{configPath}'"); + } + foreach (XElement element in mainElement.Elements()) { list.Add(new LevelObjectPrefab(element)); } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs index 0d27b15b7..b64d9bb45 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs @@ -272,7 +272,7 @@ namespace Barotrauma attack.Afflictions.Clear(); foreach (Affliction affliction in multipliedAfflictions) { - attack.Afflictions.Add(affliction); + attack.Afflictions.Add(affliction, null); } attacks.Add(attack); break; @@ -313,7 +313,7 @@ namespace Barotrauma if (entity is Character character) { if (character.CurrentHull != null) return false; - if (character.ConfigPath == Character.HumanConfigFile) + if (character.IsHuman) { if (!triggeredBy.HasFlag(TriggererType.Human)) return false; } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs index c096d8443..bf80c5fa6 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs @@ -35,61 +35,61 @@ namespace Barotrauma.RuinGeneration public string Name => "RuinGenerationParams"; - [Serialize("5000,5000", false), Editable()] + [Serialize("5000,5000", false), Editable] public Point SizeMin { get; set; } - [Serialize("8000,8000", false), Editable()] + [Serialize("8000,8000", false), Editable] public Point SizeMax { get; set; } - [Serialize(3, false), Editable(MinValueInt = 1, MaxValueInt = 10, ToolTip = "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the minimum number of times the split is done.")] + [Serialize(3, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the minimum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)] public int RoomDivisionIterationsMin { get; set; } - [Serialize(4, false), Editable(MinValueInt = 1, MaxValueInt = 10, ToolTip = "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the maximum number of times the split is done.")] + [Serialize(4, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the maximum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)] public int RoomDivisionIterationsMax { get; set; } - [Serialize(0.5f, false), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.9f, ToolTip = "The probability for the split algorithm to split the area vertically. High values tend to create tall, vertical rooms, and low values wide, horizontal rooms.")] + [Serialize(0.5f, false, description: "The probability for the split algorithm to split the area vertically. High values tend to create tall, vertical rooms, and low values wide, horizontal rooms."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.9f)] public float VerticalSplitProbability { get; set; } - [Serialize(400, false), Editable(ToolTip = "The splitting algorithm attempts to keep the dimensions the split areas larger than this. For example, if the width of the split areas would be smaller than this after a vertical split, the algorithm will do a horizontal split.")] + [Serialize(400, false, description: "The splitting algorithm attempts to keep the dimensions the split areas larger than this. For example, if the width of the split areas would be smaller than this after a vertical split, the algorithm will do a horizontal split."), Editable] public int MinSplitWidth { get; set; } - [Serialize("0.5,0.9", false), Editable(ToolTip = "The minimum and maximum width of a room relative to the areas created by the split algorithm.")] + [Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable] public Vector2 RoomWidthRange { get; set; } - [Serialize("0.5,0.9", false), Editable(ToolTip = "The minimum and maximum height of a room relative to the areas created by the split algorithm.")] + [Serialize("0.5,0.9", false, description: "The minimum and maximum height of a room relative to the areas created by the split algorithm."), Editable] public Vector2 RoomHeightRange { get; set; } - [Serialize("200,256", false), Editable(ToolTip = "The minimum and maximum width of the corridors between rooms.")] + [Serialize("200,256", false, description: "The minimum and maximum width of the corridors between rooms."), Editable] public Point CorridorWidthRange { get; @@ -140,8 +140,19 @@ namespace Barotrauma.RuinGeneration foreach (string configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) { XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc?.Root == null) continue; - var newParams = new RuinGenerationParams(doc.Root) + if (doc == null) { continue; } + var mainElement = doc.Root; + if (doc.Root.IsOverride()) + { + mainElement = doc.Root.FirstElement(); + paramsList.Clear(); + DebugConsole.NewMessage($"Overriding all ruin configuration parameters using the file {configFile}.", Color.Yellow); + } + else if (paramsList.Any()) + { + DebugConsole.NewMessage($"Adding additional ruin configuration parameters from file '{configFile}'"); + } + var newParams = new RuinGenerationParams(mainElement) { filePath = configFile }; @@ -164,7 +175,7 @@ namespace Barotrauma.RuinGeneration if (configFile != generationParams.filePath) continue; XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc?.Root == null) continue; + if (doc == null) { continue; } SerializableProperty.SerializeProperties(generationParams, doc.Root); @@ -202,34 +213,34 @@ namespace Barotrauma.RuinGeneration private set; } = new Dictionary(); - [Serialize(RoomPlacement.Any, false), Editable()] + [Serialize(RoomPlacement.Any, false), Editable] public RoomPlacement Placement { get; set; } - [Serialize(0, false), Editable()] + [Serialize(0, false), Editable] public int PlacementOffset { get; set; } - [Serialize(false, false), Editable()] + [Serialize(false, false), Editable] public bool IsCorridor { get; set; } - [Serialize(1.0f, false), Editable()] + [Serialize(1.0f, false), Editable] public float MinWaterAmount { get; set; } - [Serialize(1.0f, false), Editable()] + [Serialize(1.0f, false), Editable] public float MaxWaterAmount { get; @@ -380,11 +391,11 @@ namespace Barotrauma.RuinGeneration [Serialize(Alignment.Bottom, false), Editable] public Alignment Alignment { get; private set; } - [Serialize("0,0", false), Editable(ToolTip = "Minimum offset from the anchor position, relative to the size of the room."+ - " For example, a value of { -0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-left corner of the room and bottom-center.")] + [Serialize("0,0", false, description: "Minimum offset from the anchor position, relative to the size of the room." + + " For example, a value of { -0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-left corner of the room and bottom-center."), Editable] public Vector2 MinOffset { get; private set; } - [Serialize("0,0", false), Editable(ToolTip = "Maximum offset from the anchor position, relative to the size of the room." + - " For example, a value of { 0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-right corner of the room and bottom-center.")] + [Serialize("0,0", false, description: "Maximum offset from the anchor position, relative to the size of the room." + + " For example, a value of { 0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-right corner of the room and bottom-center."), Editable] public Vector2 MaxOffset { get; private set; } [Serialize(RuinEntityType.Prop, false), Editable] diff --git a/Barotrauma/BarotraumaShared/Source/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/Source/Map/LinkedSubmarine.cs index 5f1a5b31c..8db26524f 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/LinkedSubmarine.cs @@ -33,6 +33,15 @@ namespace Barotrauma private bool loadSub; private Submarine sub; + private ushort originalMyPortID; + + //the ID of the docking port the sub was docked to in the original sub file + //(needed when replacing a lost sub) + private ushort originalLinkedToID; + private DockingPort originalLinkedPort; + + private bool purchasedLostShuttles; + public Submarine Sub { get @@ -139,13 +148,13 @@ namespace Barotrauma public static LinkedSubmarine Load(XElement element, Submarine submarine) { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); - LinkedSubmarine linkedSub = null; if (Screen.Selected == GameMain.SubEditorScreen) { linkedSub = CreateDummy(submarine, element, pos); linkedSub.saveElement = element; + linkedSub.purchasedLostShuttles = false; } else { @@ -154,35 +163,37 @@ namespace Barotrauma saveElement = element }; + linkedSub.purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles; string levelSeed = element.GetAttributeString("location", ""); - if (!string.IsNullOrWhiteSpace(levelSeed) && GameMain.GameSession.Level != null && GameMain.GameSession.Level.Seed != levelSeed) + if (!string.IsNullOrWhiteSpace(levelSeed) && + GameMain.GameSession.Level != null && + GameMain.GameSession.Level.Seed != levelSeed && + !linkedSub.purchasedLostShuttles) { linkedSub.loadSub = false; - return null; } - - linkedSub.loadSub = true; - - linkedSub.rect.Location = MathUtils.ToPoint(pos); + else + { + linkedSub.loadSub = true; + linkedSub.rect.Location = MathUtils.ToPoint(pos); + } } linkedSub.filePath = element.GetAttributeString("filepath", ""); - - string linkedToString = element.GetAttributeString("linkedto", ""); - if (linkedToString != "") + int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]); + for (int i = 0; i < linkedToIds.Length; i++) { - string[] linkedToIds = linkedToString.Split(','); - for (int i = 0; i < linkedToIds.Length; i++) - { - linkedSub.linkedToID.Add((ushort)int.Parse(linkedToIds[i])); - } + linkedSub.linkedToID.Add((ushort)linkedToIds[i]); } - return linkedSub; + linkedSub.originalLinkedToID = (ushort)element.GetAttributeInt("originallinkedto", 0); + linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0); + + return linkedSub.loadSub ? linkedSub : null; } public override void OnMapLoaded() { - if (!loadSub) return; + if (!loadSub) { return; } sub = Submarine.Load(saveElement, false); @@ -196,7 +207,6 @@ namespace Barotrauma sub.SetPosition(WorldPosition); } - DockingPort linkedPort = null; DockingPort myPort = null; @@ -212,36 +222,78 @@ namespace Barotrauma if (linkedPort == null) { - return; - } - - float closestDistance = 0.0f; - foreach (DockingPort port in DockingPort.List) - { - if (port.Item.Submarine != sub || port.IsHorizontal != linkedPort.IsHorizontal) continue; - - float dist = Vector2.Distance(port.Item.WorldPosition, linkedPort.Item.WorldPosition); - if (myPort == null || dist < closestDistance) + if (purchasedLostShuttles) { - myPort = port; - closestDistance = dist; + linkedPort = (FindEntityByID(originalLinkedToID) as Item)?.GetComponent(); + } + if (linkedPort == null) { return; } + } + originalLinkedPort = linkedPort; + + myPort = (FindEntityByID(originalMyPortID) as Item)?.GetComponent(); + if (myPort == null) + { + float closestDistance = 0.0f; + foreach (DockingPort port in DockingPort.List) + { + if (port.Item.Submarine != sub || port.IsHorizontal != linkedPort.IsHorizontal) { continue; } + float dist = Vector2.Distance(port.Item.WorldPosition, linkedPort.Item.WorldPosition); + if (myPort == null || dist < closestDistance) + { + myPort = port; + closestDistance = dist; + } } } if (myPort != null) { + originalMyPortID = myPort.Item.ID; + myPort.Undock(); - Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition; - Vector2 offset = (myPort.IsHorizontal ? - Vector2.UnitX * Math.Sign(linkedPort.Item.WorldPosition.X - myPort.Item.WorldPosition.X) : - Vector2.UnitY * Math.Sign(linkedPort.Item.WorldPosition.Y - myPort.Item.WorldPosition.Y)); - offset *= myPort.DockedDistance; + //something else is already docked to the port this sub should be docked to + //may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be, + //and the shuttle is then restored in the campaign mode + //or if the user connects multiple subs to the same docking ports in the sub editor + if (linkedPort.Docked && linkedPort.DockingTarget != null && linkedPort.DockingTarget != myPort) + { + //just spawn below the main sub + sub.SetPosition( + linkedPort.Item.Submarine.WorldPosition - + new Vector2(0, linkedPort.Item.Submarine.GetDockedBorders().Height / 2 + sub.GetDockedBorders().Height / 2)); + } + else + { + Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition; + Vector2 offset = (myPort.IsHorizontal ? + Vector2.UnitX * Math.Sign(linkedPort.Item.WorldPosition.X - myPort.Item.WorldPosition.X) : + Vector2.UnitY * Math.Sign(linkedPort.Item.WorldPosition.Y - myPort.Item.WorldPosition.Y)); + offset *= myPort.DockedDistance; - sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset); + sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset); - myPort.Dock(linkedPort); - myPort.Lock(true); + myPort.Dock(linkedPort); + myPort.Lock(true); + } + } + + if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles) + { + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine != sub) { continue; } + for (int i = 0; i < wall.SectionCount; i++) + { + wall.AddDamage(i, -wall.Prefab.Health); + } + } + foreach (Hull hull in Hull.hullList) + { + if (hull.Submarine != sub) { continue; } + hull.WaterVolume = 0.0f; + hull.OxygenPercentage = 100.0f; + } } sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition); @@ -258,9 +310,7 @@ namespace Barotrauma { var doc = Submarine.OpenFile(filePath); saveElement = doc.Root; - saveElement.Name = "LinkedSubmarine"; - saveElement.Add(new XAttribute("filepath", filePath)); } else @@ -274,7 +324,7 @@ namespace Barotrauma var linkedPort = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent() != null); if (linkedPort != null) { - if (saveElement.Attribute("linkedto") != null) saveElement.Attribute("linkedto").Remove(); + saveElement.Attribute("linkedto")?.Remove(); saveElement.Add(new XAttribute("linkedto", linkedPort.ID)); } } @@ -284,6 +334,11 @@ namespace Barotrauma sub.SaveToXElement(saveElement); } + saveElement.Attribute("originallinkedto")?.Remove(); + saveElement.Add(new XAttribute("originallinkedto", originalLinkedPort != null ? originalLinkedPort.Item.ID : originalLinkedToID)); + saveElement.Attribute("originalmyport")?.Remove(); + saveElement.Add(new XAttribute("originalmyport", originalMyPortID)); + if (sub != null) { bool leaveBehind = false; @@ -300,24 +355,19 @@ namespace Barotrauma } } - if (leaveBehind) { saveElement.SetAttributeValue("location", Level.Loaded.Seed); saveElement.SetAttributeValue("worldpos", XMLExtensions.Vector2ToString(sub.SubBody.Position)); - } else { if (saveElement.Attribute("location") != null) saveElement.Attribute("location").Remove(); if (saveElement.Attribute("worldpos") != null) saveElement.Attribute("worldpos").Remove(); } - saveElement.SetAttributeValue("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)); } - - parentElement.Add(saveElement); return saveElement; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/Source/Map/Map/LocationType.cs index b24d55c4b..223596b79 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Map/LocationType.cs @@ -106,12 +106,10 @@ namespace Barotrauma if (jobIdentifier == "") { DebugConsole.ThrowError("Error in location type \""+ Identifier + "\" - hireable jobs should be configured using identifiers instead of names."); - jobIdentifier = subElement.GetAttributeString("name", ""); - jobPrefab = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == jobIdentifier.ToLowerInvariant()); } else { - jobPrefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == jobIdentifier.ToLowerInvariant()); + jobPrefab = JobPrefab.Get(jobIdentifier.ToLowerInvariant()); } if (jobPrefab == null) { @@ -124,7 +122,7 @@ namespace Barotrauma hireableJobs.Add(hireableJob); break; case "symbol": - symbolSprite = new Sprite(subElement); + symbolSprite = new Sprite(subElement, lazyLoad: true); SpriteColor = subElement.GetAttributeColor("color", Color.White); break; case "changeto": @@ -192,14 +190,50 @@ namespace Barotrauma public static void Init() { var locationTypeFiles = GameMain.Instance.GetFilesOfType(ContentType.LocationTypes); - foreach (string file in locationTypeFiles) { XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc?.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) + if (doc == null) { continue; } + var mainElement = doc.Root; + if (doc.Root.IsOverride()) { + mainElement = doc.Root.FirstElement(); + DebugConsole.NewMessage($"Overriding all location types with '{file}'", Color.Yellow); + List.Clear(); + } + else if (List.Any()) + { + DebugConsole.NewMessage($"Loading additional location types from file '{file}'"); + } + 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}': 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}'", Color.Yellow); + } + else + { + DebugConsole.ThrowError($"Error in '{file}': Duplicate identifier defined with the identifier '{identifier}'"); + continue; + } + } LocationType locationType = new LocationType(element); List.Add(locationType); } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Map/MapGenerationParams.cs b/Barotrauma/BarotraumaShared/Source/Map/Map/MapGenerationParams.cs index 9acff82e9..0a0556753 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Map/MapGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Map/MapGenerationParams.cs @@ -36,16 +36,16 @@ namespace Barotrauma public readonly bool ShowOverlay = true; #endif - [Serialize(6, true)] + [Serialize(6, true)] public int DifficultyZones { get; set; } //Number of difficulty zones - + [Serialize(2000, true)] public int Size { get; set; } - [Serialize(20.0f, true), Editable(0.0f, 5000.0f, ToolTip = "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters).")] + [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)] public float SmallLevelConnectionLength { get; set; } - [Serialize(200.0f, true), Editable(0.0f, 5000.0f, ToolTip = "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters).")] + [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)] public float LargeLevelConnectionLength { get; set; } [Serialize(1024, true)] @@ -65,77 +65,71 @@ namespace Barotrauma [Serialize("280,80", true), Editable] public Vector2 TileSpriteSpacing { get; set; } - [Serialize(1.0f, true), Editable(0.0f, 1.0f, ToolTip = "How dark the center of the map is (1.0f = black).")] - public float CenterDarkenStrength { get; set; } + [Serialize(1.0f, true, description: "How dark the center of the map is (1.0f = black)."), Editable(0.0f, 1.0f)] + public float CenterDarkenStrength { get; set; } - [Serialize(0.9f, true), Editable(0.0f, 1.0f, ToolTip = "How close to the center the darkening starts (0.8f = 20% from the edge).")] + [Serialize(0.9f, true, description: "How close to the center the darkening starts (0.8f = 20% from the edge)."), Editable(0.0f, 1.0f)] public float CenterDarkenRadius { get; set; } - [Serialize(5, true), Editable(0, 1000, - ToolTip = "The edge of the dark center area is wave-shaped, and the frequency is determined by this value." + - " I.e. how many points does the star-shaped dark area in the center have.")] + [Serialize(5, true, description: "The edge of the dark center area is wave-shaped, and the frequency is determined by this value." + + " I.e. how many points does the star-shaped dark area in the center have."), Editable(0, 1000)] public int CenterDarkenWaveFrequency { get; set; } - [Serialize(15.0f, true), Editable(0, 1000.0f, - ToolTip = "How heavily the noise map affects the phase of the edge wave (higher value = more irregular shape).")] + [Serialize(15.0f, true, description: "How heavily the noise map affects the phase of the edge wave (higher value = more irregular shape)."), Editable(0, 1000.0f)] public float CenterDarkenWavePhaseNoise { get; set; } - [Serialize(0.8f, true), Editable(0.0f, 1.0f, ToolTip = "How dark the edges of the map are (1.0f = black).")] + [Serialize(0.8f, true, description: "How dark the edges of the map are (1.0f = black)."), Editable(0.0f, 1.0f)] public float EdgeDarkenStrength { get; set; } - [Serialize(0.9f, true), Editable(0.0f, 1.0f, ToolTip = "How far from the center the darkening starts (0.95f = 5% from the edge).")] + [Serialize(0.9f, true, description: "How far from the center the darkening starts (0.95f = 5% from the edge)."), Editable(0.0f, 1.0f)] public float EdgeDarkenRadius { get; set; } - - [Serialize(0.9f, true), Editable(0.0f, 1.0f, ToolTip = "How far from the center locations can be placed.")] + + [Serialize(0.9f, true, description: "How far from the center locations can be placed."), Editable(0.0f, 1.0f)] public float LocationRadius { get; set; } - - [Serialize(20.0f, true), Editable(1.0f, 100.0f, - ToolTip = "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)") ] + + [Serialize(20.0f, true, 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(1.0f, 100.0f)] public float VoronoiSiteInterval { get; set; } - - [Serialize(0.3f, true), Editable(0.01f, 1.0f, - ToolTip = "How likely it is for a site to be placed at a given spot (e.g. 20% probability for a site to be placed every 5 units of the map). "+ - "Multiplied with the noise value in the spot, meaning that sites are less likely to appear in dark spots.")] + + [Serialize(0.3f, true, description: "How likely it is for a site to be placed at a given spot (e.g. 20% probability for a site to be placed every 5 units of the map). " + + "Multiplied with the noise value in the spot, meaning that sites are less likely to appear in dark spots."), Editable(0.01f, 1.0f)] public float VoronoiSitePlacementProbability { get; set; } - - [Serialize(0.1f, true), Editable(0.01f, 1.0f, - ToolTip = "Probability * noise ^ 2 must be higher than this for a site to be placed. "+ - "= How bright the noise map must be at a given spot for a location to be placed there")] + + [Serialize(0.1f, true, description: "Probability * noise ^ 2 must be higher than this for a site to be placed. " + + "= How bright the noise map must be at a given spot for a location to be placed there"), Editable(0.01f, 1.0f)] public float VoronoiSitePlacementMinVal { get; set; } - [Serialize(10.0f, true), Editable(0.0f, 500.0f, ToolTip = "Connections smaller than this are removed.")] + [Serialize(10.0f, true, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)] public float MinConnectionDistance { get; set; } - - [Serialize(5.0f, true), Editable(0.0f, 100.0f, ToolTip = "Locations that are closer than this to another location are removed.")] + + [Serialize(5.0f, true, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)] public float MinLocationDistance { get; set; } - [Serialize(0.2f, true), Editable(0.0f, 10.0f, - ToolTip = "Affects how many iterations are done when generating the jagged shape of the connections (iterations = Sqrt(connectionLength * multiplier)).")] + [Serialize(0.2f, true, description: "Affects how many iterations are done when generating the jagged shape of the connections (iterations = Sqrt(connectionLength * multiplier))."), Editable(0.0f, 10.0f)] public float ConnectionIterationMultiplier { get; set; } - - [Serialize(0.5f, true), Editable(0.0f, 10.0f, ToolTip = "How large the \"bends\" in the connections are (displacement = connectionLength * multiplier).")] + + [Serialize(0.5f, true, description: "How large the \"bends\" in the connections are (displacement = connectionLength * multiplier)."), Editable(0.0f, 10.0f)] public float ConnectionDisplacementMultiplier { get; set; } - - [Serialize(0.1f, true), Editable(0.0f, 10.0f, ToolTip = "ConnectionIterationMultiplier for the UI indicator lines between locations.")] + + [Serialize(0.1f, true, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f)] public float ConnectionIndicatorIterationMultiplier { get; set; } - - [Serialize(0.1f, true), Editable(0.0f, 10.0f, ToolTip = "ConnectionDisplacementMultiplier for the UI indicator lines between locations.")] + + [Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f)] public float ConnectionIndicatorDisplacementMultiplier { get; set; } public Sprite ConnectionSprite { get; private set; } #if CLIENT - - [Serialize(15.0f, true), Editable(1.0f, 1000.0f, ToolTip = "Size of the location icons in pixels when at 100% zoom.")] + + [Serialize(15.0f, true, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] public float LocationIconSize { get; set; } - [Serialize("150,150,150,255", true), Editable(ToolTip = "The color used to display the low-difficulty connections on the map.")] + [Serialize("150,150,150,255", true, description: "The color used to display the low-difficulty connections on the map."), Editable()] public Color LowDifficultyColor { get; set; } - [Serialize("210,143,83,255", true), Editable(ToolTip = "The color used to display the medium-difficulty connections on the map.")] + [Serialize("210,143,83,255", true, description: "The color used to display the medium-difficulty connections on the map."), Editable()] public Color MediumDifficultyColor { get; set; } - [Serialize("216,154,138", true), Editable(ToolTip = "The color used to display the high-difficulty connections on the map.")] + [Serialize("216,154,138", true, description: "The color used to display the high-difficulty connections on the map."), Editable()] public Color HighDifficultyColor { get; set; } public SpriteSheet DecorativeMapSprite { get; private set; } @@ -172,14 +166,35 @@ namespace Barotrauma 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; foreach (string file in files) { XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc?.Root == null) return; - - instance = new MapGenerationParams(doc.Root); - break; + 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}'", Color.Yellow); + } + } + else if (selectedElement != null) + { + DebugConsole.ThrowError($"Error in {file}: Another map generation parameter file already loaded! Use tags to override it."); + break; + } + selectedElement = mainElement; + } + if (selectedElement == null) + { + DebugConsole.ThrowError("Could not find a valid element in the map generation parameter files!"); + } + else + { + instance = new MapGenerationParams(selectedElement); } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs index 5510fad73..d4b911cce 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Xml.Linq; namespace Barotrauma { @@ -303,6 +304,43 @@ namespace Barotrauma public static object GetSelected() { return (object)selected; - } + } + + protected bool HandleExisting(string identifier, bool allowOverriding, string file = null) + { + if (!string.IsNullOrEmpty(identifier)) + { + MapEntityPrefab existingPrefab = List.Find(e => e.Identifier == identifier); + if (existingPrefab != null) + { + if (allowOverriding) + { + string msg = $"Overriding an existing map entity with the identifier '{identifier}'"; + if (!string.IsNullOrWhiteSpace(file)) + { + msg += $" using the file '{file}'"; + } + msg += "."; + DebugConsole.NewMessage(msg, Color.Yellow); + List.Remove(existingPrefab); + } + else + { + if (!string.IsNullOrWhiteSpace(file)) + { + DebugConsole.ThrowError($"Error in '{file}': Map entity prefabs \"" + name + "\" and \"" + existingPrefab.Name + "\" have the same identifier! " + + "Use the XML element as the parent of the map element's definition to override the existing map element."); + } + else + { + DebugConsole.ThrowError("Map entity prefabs \"" + name + "\" and \"" + existingPrefab.Name + "\" have the same identifier! " + + "Use the XML element as the parent of the map element's definition to override the existing map element."); + } + return false; + } + } + } + return true; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs index 72be3d368..4836c9c69 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs @@ -151,7 +151,7 @@ namespace Barotrauma private set; } - [Serialize("0,0", true), Editable(ToolTip = "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", 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.")] public Vector2 DropShadowOffset { get; @@ -238,6 +238,29 @@ namespace Barotrauma } } } + + //for upgrading the dimensions of a structure from xml + [Serialize(0, false)] + public int RectWidth + { + get { return rect.Width; } + set + { + if (value <= 0) { return; } + Rect = new Rectangle(rect.X, rect.Y, value, rect.Height); + } + } + //for upgrading the dimensions of a structure from xml + [Serialize(0, false)] + public int RectHeight + { + get { return rect.Height; } + set + { + if (value <= 0) { return; } + Rect = new Rectangle(rect.X, rect.Y, rect.Width, value); + } + } public float BodyWidth { @@ -412,19 +435,20 @@ namespace Barotrauma private void CreateStairBodies() { Bodies = new List(); + + float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f)); - float bodyWidth = ConvertUnits.ToSimUnits(rect.Width * Math.Sqrt(2.0)); + float bodyWidth = ConvertUnits.ToSimUnits(rect.Width / Math.Cos(stairAngle)); float bodyHeight = ConvertUnits.ToSimUnits(10); + float stairHeight = rect.Width * (float)Math.Tan(stairAngle); + Body newBody = BodyFactory.CreateRectangle(GameMain.World, bodyWidth, bodyHeight, 1.5f); newBody.BodyType = BodyType.Static; - Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height + rect.Width / 2.0f); - /*stairPos += new Vector2( - (StairDirection == Direction.Right) ? -Submarine.GridSize.X * 1.5f : Submarine.GridSize.X * 1.5f, - -Submarine.GridSize.Y * 2.0f);*/ - newBody.Rotation = (StairDirection == Direction.Right) ? MathHelper.PiOver4 : -MathHelper.PiOver4; + Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height + stairHeight / 2.0f); + newBody.Rotation = (StairDirection == Direction.Right) ? stairAngle : -stairAngle; newBody.CollisionCategories = Physics.CollisionStairs; newBody.Friction = 0.8f; newBody.UserData = this; @@ -1191,6 +1215,11 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(s, element); + if (submarine?.GameVersion != null) + { + SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.GameVersion); + } + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString()) diff --git a/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs index 34c1170b5..9e10e2963 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs @@ -102,6 +102,13 @@ namespace Barotrauma private set; } + [Serialize(45.0f, false)] + public float StairAngle + { + get; + private set; + } + [Serialize(false, false)] public bool NoAITarget { @@ -152,18 +159,39 @@ namespace Barotrauma foreach (string filePath in filePaths) { XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) return; - - foreach (XElement el in doc.Root.Elements()) - { - StructurePrefab sp = Load(el); - - List.Add(sp); + if (doc == null) { return; } + var rootElement = doc.Root; + if (rootElement.IsOverride()) + { + foreach (var element in rootElement.Elements()) + { + foreach (var childElement in element.Elements()) + { + Load(childElement, true); + } + } + } + else + { + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + foreach (var childElement in element.Elements()) + { + Load(childElement, true); + } + } + else + { + Load(element, false); + } + } } } } - public static StructurePrefab Load(XElement element) + public static StructurePrefab Load(XElement element, bool allowOverride) { StructurePrefab sp = new StructurePrefab { @@ -275,16 +303,10 @@ namespace Barotrauma 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."); } - if (!string.IsNullOrEmpty(sp.identifier)) + if (sp.HandleExisting(sp.Identifier, allowOverride)) { - MapEntityPrefab existingPrefab = List.Find(e => e.Identifier == sp.identifier); - if (existingPrefab != null) - { - DebugConsole.ThrowError( - "Map entity prefabs \"" + sp.name + "\" and \"" + existingPrefab.Name + "\" have the same identifier!"); - } + List.Add(sp); } - return sp; } @@ -315,7 +337,7 @@ namespace Barotrauma } if (ResizeVertical && Math.Abs(placeSize.Y) < Submarine.GridSize.Y) { - placeSize.X = Submarine.GridSize.Y; + placeSize.Y = Submarine.GridSize.Y; } newRect = Submarine.AbsRect(placePosition, placeSize); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs index c7ebbc0a2..9b7991ad8 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs @@ -246,6 +246,23 @@ namespace Barotrauma } } + private bool? subsLeftBehind; + public bool SubsLeftBehind + { + get + { + if (subsLeftBehind.HasValue) { return subsLeftBehind.Value; } + + CheckSubsLeftBehind(); + return subsLeftBehind.Value; + } + //set { subsLeftBehind = value; } + } + public bool LeftBehindSubDockingPortOccupied + { + get; private set; + } + public new Vector2 DrawPosition { get; @@ -306,6 +323,20 @@ namespace Barotrauma private set; } + private bool? requiredContentPackagesInstalled; + public bool RequiredContentPackagesInstalled + { + get + { + if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } + return RequiredContentPackages.All(cp => GameMain.SelectedPackages.Any(cp2 => cp2.Name == cp)); + } + set + { + requiredContentPackagesInstalled = value; + } + } + //constructors & generation ---------------------------------------------------- public Submarine(string filePath, string hash = "", bool tryLoad = true) : base(null) @@ -325,6 +356,8 @@ namespace Barotrauma this.hash = new Md5Hash(hash); } + IsFileCorrupted = false; + if (tryLoad) { XDocument doc = null; @@ -376,6 +409,8 @@ namespace Barotrauma { RequiredContentPackages.Add(contentPackageName); } + + CheckSubsLeftBehind(doc.Root); #if CLIENT string previewImageData = doc.Root.GetAttributeString("previewimage", ""); if (!string.IsNullOrEmpty(previewImageData)) @@ -437,6 +472,42 @@ namespace Barotrauma tags &= ~tag; } + public void CheckSubsLeftBehind(XElement element = null) + { + if (element == null) + { + XDocument doc = null; + int maxLoadRetries = 4; + for (int i = 0; i <= maxLoadRetries; i++) + { + doc = OpenFile(filePath, out Exception e); + if (e != null && !(e is IOException)) { break; } + if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } + DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); + Thread.Sleep(250); + } + if (doc?.Root == null) { return; } + element = doc.Root; + } + + subsLeftBehind = false; + LeftBehindSubDockingPortOccupied = false; + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() != "linkedsubmarine") { continue; } + if (subElement.Attribute("location") == null) { continue; } + + subsLeftBehind = true; + ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0); + 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) + { + LeftBehindSubDockingPortOccupied = true; + } + } + } + public void MakeOutpost() { IsOutpost = true; @@ -931,7 +1002,7 @@ namespace Barotrauma parents.Add(this); flippedX = !flippedX; - + Item.UpdateHulls(); List bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null); @@ -975,6 +1046,8 @@ namespace Barotrauma } entityGrid = Hull.GenerateEntityGrid(this); + SubBody.FlipX(); + foreach (MapEntity mapEntity in subEntities) { mapEntity.Move(HiddenSubPosition); @@ -1314,6 +1387,12 @@ namespace Barotrauma { stream = SaveUtil.DecompressFiletoStream(file); } + catch (FileNotFoundException e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found)"); + return null; + } catch (Exception e) { exception = e; @@ -1377,7 +1456,11 @@ namespace Barotrauma DebugConsole.NewMessage("Loading the submarine \"" + Name + "\" failed, retrying in 250 ms..."); Thread.Sleep(250); } - if (doc == null || doc.Root == null) { return; } + if (doc == null || doc.Root == null) + { + IsFileCorrupted = true; + return; + } submarineElement = doc.Root; } @@ -1574,6 +1657,8 @@ namespace Barotrauma if (e.Submarine != this || !e.ShouldBeSaved) continue; e.Save(element); } + + CheckSubsLeftBehind(element); } diff --git a/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs index 95575a22c..caf6610b3 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs @@ -26,6 +26,12 @@ namespace Barotrauma public const float DamageDepth = -30000.0f; private const float ImpactDamageMultiplier = 10.0f; + //limbs with a mass smaller than this won't cause an impact when they hit the sub + private const float MinImpactLimbMass = 10.0f; + //impacts smaller than this are ignored + private const float MinCollisionImpact = 3.0f; + //impacts are clamped below this value + private const float MaxCollisionImpact = 5.0f; private const float Friction = 0.2f, Restitution = 0.0f; public List HullVertices @@ -94,11 +100,11 @@ namespace Barotrauma else { List convexHull = GenerateConvexHull(); - HullVertices = convexHull; for (int i = 0; i < convexHull.Count; i++) { convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]); } + HullVertices = convexHull; Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; @@ -217,7 +223,6 @@ namespace Barotrauma Body = new PhysicsBody(farseerBody); } - private List GenerateConvexHull() { List subWalls = Structure.WallList.FindAll(wall => wall.Submarine == submarine); @@ -405,6 +410,16 @@ namespace Barotrauma depthDamageTimer = 10.0f; } + public void FlipX() + { + List convexHull = GenerateConvexHull(); + for (int i = 0; i < convexHull.Count; i++) + { + convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]); + } + HullVertices = convexHull; + } + public bool OnCollision(Fixture f1, Fixture f2, Contact contact) { if (f2.Body.UserData is Limb limb) @@ -418,19 +433,19 @@ namespace Barotrauma return CheckCharacterCollision(contact, character); } - contact.GetWorldManifold(out Vector2 normal, out FixedArray2 points); + contact.GetWorldManifold(out Vector2 normal, out FixedArray2 _); if (contact.FixtureA.Body == f1.Body) { normal = -normal; } - if (f2.UserData is VoronoiCell cell) + if (f2.UserData is VoronoiCell) { HandleLevelCollision(contact, normal); return true; } - if (f2.Body.UserData is Structure structure) + if (f2.Body.UserData is Structure) { HandleLevelCollision(contact, normal); @@ -481,18 +496,19 @@ namespace Barotrauma private void HandleLimbCollision(Contact contact, Limb limb) { - if (limb.Mass > 100.0f) + if (limb.Mass > MinImpactLimbMass) { - Vector2 normal = Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ? + Vector2 normal = + Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ? Vector2.UnitY : Vector2.Normalize(Body.SimPosition - limb.SimPosition); - float impact = Math.Min(Vector2.Dot(Velocity - limb.LinearVelocity, -normal), 50.0f) / 5.0f * Math.Min(limb.Mass / 200.0f, 1); + float impact = Math.Min(Vector2.Dot(Velocity - limb.LinearVelocity, -normal), 50.0f) * Math.Min(limb.Mass / 100.0f, 1); - ApplyImpact(impact, -normal, contact, applyDamage: false); + ApplyImpact(impact, normal, contact, applyDamage: false); foreach (Submarine dockedSub in submarine.DockedTo) { - dockedSub.SubBody.ApplyImpact(impact, -normal, contact, applyDamage: false); + dockedSub.SubBody.ApplyImpact(impact, normal, contact, applyDamage: false); } } @@ -558,9 +574,7 @@ namespace Barotrauma float damageAmount = contactDot * Body.Mass / limb.character.Mass; - Vector2 n; - FixedArray2 contactPos; - contact.GetWorldManifold(out n, out contactPos); + contact.GetWorldManifold(out _, out FixedArray2 contactPos); limb.character.LastDamageSource = submarine; limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(contactPos[0]), limb, new List() { AfflictionPrefab.InternalDamage.Instantiate(damageAmount) }, 0.0f, true, 0.0f); @@ -587,9 +601,7 @@ namespace Barotrauma } #if CLIENT - Vector2 n; - FixedArray2 particlePos; - contact.GetWorldManifold(out n, out particlePos); + contact.GetWorldManifold(out _, out FixedArray2 particlePos); int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); for (int i = 0; i < particleAmount; i++) @@ -605,9 +617,7 @@ namespace Barotrauma { Debug.Assert(otherSub != submarine); - Vector2 normal; - FixedArray2 points; - contact.GetWorldManifold(out normal, out points); + contact.GetWorldManifold(out Vector2 normal, out FixedArray2 points); if (contact.FixtureA.Body == otherSub.SubBody.Body.FarseerBody) { normal = -normal; @@ -688,15 +698,13 @@ namespace Barotrauma private void ApplyImpact(float impact, Vector2 direction, Contact contact, bool applyDamage = true) { - float minImpact = 3.0f; - - if (impact < minImpact) { return; } + if (impact < MinCollisionImpact) { return; } contact.GetWorldManifold(out Vector2 tempNormal, out FixedArray2 worldPoints); Vector2 lastContactPoint = worldPoints[0]; Vector2 impulse = direction * impact * 0.5f; - impulse = impulse.ClampLength(5.0f); + impulse = impulse.ClampLength(MaxCollisionImpact); if (!MathUtils.IsValid(impulse)) { @@ -719,23 +727,38 @@ namespace Barotrauma if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { GameMain.GameScreen.Cam.Shake = impact * 2.0f; - float angularVelocity = - (lastContactPoint.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y - - (lastContactPoint.Y - Body.SimPosition.Y) / ConvertUnits.ToSimUnits(submarine.Borders.Height / 2) * impulse.X; - GameMain.GameScreen.Cam.AngularVelocity = MathHelper.Clamp(angularVelocity * 0.1f, -1.0f, 1.0f); + if (!submarine.IsOutpost && !submarine.DockedTo.Any(s => s.IsOutpost)) + { + float angularVelocity = + (lastContactPoint.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y + - (lastContactPoint.Y - Body.SimPosition.Y) / ConvertUnits.ToSimUnits(submarine.Borders.Height / 2) * impulse.X; + GameMain.GameScreen.Cam.AngularVelocity = MathHelper.Clamp(angularVelocity * 0.1f, -1.0f, 1.0f); + } } #endif foreach (Character c in Character.CharacterList) { - if (c.Submarine != submarine) continue; - if (impact > 2.0f) c.SetStun((impact - 2.0f) * 0.1f); + if (c.Submarine != submarine) { continue; } foreach (Limb limb in c.AnimController.Limbs) { - limb.body.ApplyLinearImpulse(limb.Mass * impulse, 20.0f); + limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f); + } + c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); + + bool holdingOntoSomething = false; + if (c.SelectedConstruction != null) + { + var controller = c.SelectedConstruction.GetComponent(); + holdingOntoSomething = controller != null && controller.LimbPositions.Any(); + } + + //stun for up to 1 second if the impact equal or higher to the maximum impact + if (impact >= MaxCollisionImpact && !holdingOntoSomething) + { + c.SetStun(Math.Min(impulse.Length() * 0.2f, 1.0f)); } - c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 20.0f); } foreach (Item item in Item.ItemList) @@ -743,7 +766,7 @@ namespace Barotrauma if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) continue; - item.body.ApplyLinearImpulse(item.body.Mass * impulse, 20.0f); + item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f); } var damagedStructures = Explosion.RangedStructureDamage( @@ -770,7 +793,7 @@ namespace Barotrauma "StructureBlunt", impact * 10.0f, ConvertUnits.ToDisplayUnits(lastContactPoint), - MathHelper.Lerp(2000.0f, 10000.0f, (impact - minImpact) / 2.0f), + MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f), maxDamageStructure.Tags); } #endif diff --git a/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs index 2fa30ef25..aa833b8a8 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs @@ -648,8 +648,8 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(jobIdentifier)) { w.assignedJob = - JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == jobIdentifier) ?? - JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == jobIdentifier); + JobPrefab.Get(jobIdentifier) ?? + JobPrefab.List.Values.FirstOrDefault(jp => jp.Name.ToLowerInvariant() == jobIdentifier); } w.ladderId = (ushort)element.GetAttributeInt("ladders", 0); diff --git a/Barotrauma/BarotraumaShared/Source/Memento.cs b/Barotrauma/BarotraumaShared/Source/Memento.cs index 17c106a4d..912d498ac 100644 --- a/Barotrauma/BarotraumaShared/Source/Memento.cs +++ b/Barotrauma/BarotraumaShared/Source/Memento.cs @@ -3,6 +3,15 @@ using System.Linq; namespace Barotrauma { + interface IMemorizable + { + Memento Memento { get; } + void StoreSnapshot(); + void Undo(); + void Redo(); + void ClearHistory(); + } + public class Memento { public T Current { get; private set; } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs index 8eab04a50..64888301a 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Networking { public const int MaxNameLength = 20; - public string Name; + public string Name; public UInt16 NameID; public byte ID; public UInt64 SteamID; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs index 87b3a9ade..128cc5199 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs @@ -72,7 +72,7 @@ namespace Barotrauma.Networking if (!File.Exists(file)) { return; } XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null || doc.Root == null) { return; } + if (doc == null) { return; } List.Clear(); foreach (XElement element in doc.Root.Elements()) diff --git a/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs index 1730122c7..0d3cba3b9 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs @@ -94,7 +94,23 @@ namespace Barotrauma public KarmaManager() { - XDocument doc = XMLExtensions.TryLoadXml(ConfigFile); + XDocument doc = null; + int maxLoadRetries = 4; + for (int i = 0; i <= maxLoadRetries; i++) + { + try + { + doc = XMLExtensions.TryLoadXml(ConfigFile); + break; + } + catch (IOException) + { + if (i == maxLoadRetries) { break; } + DebugConsole.NewMessage("Opening karma settings file \"" + ConfigFile + "\" failed, retrying in 250 ms..."); + System.Threading.Thread.Sleep(250); + } + } + SerializableProperties = SerializableProperty.DeserializeProperties(this, doc?.Root); if (doc?.Root != null) { @@ -104,7 +120,7 @@ namespace Barotrauma string presetName = subElement.GetAttributeString("name", ""); Presets[presetName.ToLowerInvariant()] = subElement; } - SelectPreset("default"); + SelectPreset(GameMain.NetworkMember?.ServerSettings?.KarmaPreset ?? "default"); } herpesAffliction = AfflictionPrefab.List.Find(ap => ap.Identifier == "spaceherpes"); } @@ -118,13 +134,18 @@ namespace Barotrauma { SerializableProperty.DeserializeProperties(this, Presets[presetName]); } + else if (Presets.ContainsKey("custom")) + { + SerializableProperty.DeserializeProperties(this, Presets["custom"]); + + } } public void SaveCustomPreset() { if (Presets.ContainsKey("custom")) { - SerializableProperty.SerializeProperties(this, Presets["custom"]); + SerializableProperty.SerializeProperties(this, Presets["custom"], saveIfDefault: true); } } @@ -143,9 +164,25 @@ namespace Barotrauma NewLineOnAttributes = true }; - using (var writer = XmlWriter.Create(ConfigFile, settings)) + int maxLoadRetries = 4; + for (int i = 0; i <= maxLoadRetries; i++) { - doc.Save(writer); + try + { + using (var writer = XmlWriter.Create(ConfigFile, settings)) + { + doc.Save(writer); + } + break; + } + catch (IOException) + { + if (i == maxLoadRetries) { throw; } + + DebugConsole.NewMessage("Saving karma settings file file \"" + ConfigFile + "\" failed, retrying in 250 ms..."); + System.Threading.Thread.Sleep(250); + continue; + } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs index 6d47913a4..2df69075a 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs @@ -34,6 +34,11 @@ namespace Barotrauma.Networking public const float HighPrioCharacterPositionUpdateInterval = 0.0f; public const float LowPrioCharacterPositionUpdateInterval = 1.0f; + //this should be higher than LowPrioCharacterPositionUpdateInterval, + //otherwise the clients may freeze characters even though the server hasn't actually stopped sending position updates + public const float FreezeCharacterIfPositionDataMissingDelay = 2.0f; + public const float DisableCharacterIfPositionDataMissingDelay = 3.5f; + public const float DeleteDisconnectedTime = 20.0f; public const float ItemConditionUpdateInterval = 0.15f; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetIdUtils.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetIdUtils.cs index c6780a008..2797603b0 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetIdUtils.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetIdUtils.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Networking { if (IdMoreRecent(min, max)) { - throw new ArgumentException("Min cannot be larger than max"); + throw new ArgumentException($"Min cannot be larger than max ({min}, {max})"); } if (!IdMoreRecent(id, min)) diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs index f85e882a8..0bf51e210 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs @@ -140,8 +140,6 @@ namespace Barotrauma.Networking public Dictionary messageCount = new Dictionary(); #endif - protected string name; - protected ServerSettings serverSettings; protected TimeSpan updateInterval; @@ -155,6 +153,12 @@ namespace Barotrauma.Networking public bool ShowNetStats; +#if DEBUG + public float SimulatedRandomLatency, SimulatedMinimumLatency; + public float SimulatedLoss; + public float SimulatedDuplicatesChance; +#endif + public int TickRate { get { return serverSettings.TickRate; } @@ -171,16 +175,6 @@ namespace Barotrauma.Networking private set; } = new KarmaManager(); - public string Name - { - get { return name; } - set - { - if (string.IsNullOrEmpty(value)) { return; } - name = value.Replace(":", "").Replace(";", ""); - } - } - public bool GameStarted { get { return gameStarted; } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs index 479d87f3b..73776fb79 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs @@ -623,6 +623,34 @@ namespace Barotrauma.Networking set; } + [Serialize(defaultValue: 90.0f, isSaveable: true)] + public float TraitorsMinStartDelay + { + get; + set; + } + + [Serialize(defaultValue: 180.0f, isSaveable: true)] + public float TraitorsMaxStartDelay + { + get; + set; + } + + [Serialize(defaultValue: 30.0f, isSaveable: true)] + public float TraitorsMinRestartDelay + { + get; + set; + } + + [Serialize(defaultValue: 90.0f, isSaveable: true)] + public float TraitorsMaxRestartDelay + { + get; + set; + } + private SelectionMode subSelectionMode; [Serialize(SelectionMode.Manual, true)] public SelectionMode SubSelectionMode @@ -693,12 +721,21 @@ namespace Barotrauma.Networking } } + private string karmaPreset = "default"; [Serialize("default", true)] public string KarmaPreset { - get; - set; - } = "default"; + get { return karmaPreset; } + set + { + if (karmaPreset == value) { return; } + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + GameMain.NetworkMember?.KarmaManager?.SelectPreset(value); + } + karmaPreset = value; + } + } [Serialize("sandbox", true)] public string GameModeIdentifier diff --git a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs index 118717f43..706d29e58 100644 --- a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs @@ -6,6 +6,8 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; +using LimbParams = Barotrauma.RagdollParams.LimbParams; +using ColliderParams = Barotrauma.RagdollParams.ColliderParams; namespace Barotrauma { @@ -358,6 +360,7 @@ namespace Barotrauma body.CollisionCategories = Physics.CollisionItem; body.Friction = limbParams.Friction; body.Restitution = limbParams.Restitution; + body.AngularDamping = limbParams.AngularDamping; body.UserData = this; SetTransformIgnoreContacts(position, 0.0f); LastSentPosition = position; diff --git a/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs index 6865d2329..56f502e56 100644 --- a/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Reflection; using System.Xml.Linq; - namespace Barotrauma { [AttributeUsage(AttributeTargets.Property)] @@ -21,10 +20,13 @@ namespace Barotrauma public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; public float ValueStep; - public string ToolTip; - public string DisplayName; + /// + /// Currently implemented only for int fields. TODO: implement the remaining types (SerializableEntityEditor) + /// + public bool ReadOnly; + public Editable(int maxLength = 20) { MaxLength = maxLength; @@ -57,6 +59,8 @@ namespace Barotrauma public bool isSaveable; public string translationTextTag; + public string Description; + /// /// Makes the property serializable to/from XML /// @@ -64,11 +68,12 @@ namespace Barotrauma /// Is the value saved to XML when serializing. /// 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. /// 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 translationTextTag = null) + public Serialize(object defaultValue, bool isSaveable, string description = "", string translationTextTag = null) { this.defaultValue = defaultValue; this.isSaveable = isSaveable; this.translationTextTag = translationTextTag; + this.Description = description; } } @@ -684,18 +689,26 @@ namespace Barotrauma { if (subElement.Name.ToString().ToLowerInvariant() != "upgrade") { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); - if (savedVersion < upgradeVersion) + if (savedVersion >= upgradeVersion) { continue; } + foreach (XAttribute attribute in subElement.Attributes()) { - foreach (XAttribute attribute in subElement.Attributes()) + string attributeName = attribute.Name.ToString().ToLowerInvariant(); + if (attributeName == "gameversion") { continue; } + if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { - string attributeName = attribute.Name.ToString().ToLowerInvariant(); - if (attributeName == "gameversion") { continue; } - if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) + property.TrySetValue(entity, attribute.Value); + } + else if (entity is Item item) + { + foreach (ISerializableEntity component in item.AllPropertyObjects) { - property.TrySetValue(entity, attribute.Value); + if (component.SerializableProperties.TryGetValue(attributeName, out SerializableProperty componentProperty)) + { + componentProperty.TrySetValue(component, attribute.Value); + } } } - } + } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs index 768018931..4385a3427 100644 --- a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs @@ -12,14 +12,7 @@ namespace Barotrauma { public static class XMLExtensions { - public static string ParseContentPathFromUri(this XObject element) - { - string[] splitted = element.BaseUri.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); - string currentFolder = Environment.CurrentDirectory.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Last(); - // Filter out the current folder -> result is "Content/blaahblaah" or "Mods/blaahblaah" etc. - IEnumerable filtered = splitted.SkipWhile(part => part != currentFolder).Skip(1); - return string.Join("/", filtered); - } + public static string ParseContentPathFromUri(this XObject element) => ToolBox.ConvertAbsoluteToRelativePath(element.BaseUri); public static XDocument TryLoadXml(string filePath) { @@ -34,9 +27,11 @@ namespace Barotrauma DebugConsole.ThrowError("Couldn't load xml document \"" + filePath + "\"!", e); return null; } - - if (doc.Root == null) 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; } @@ -559,5 +554,19 @@ namespace Barotrauma return floatArray; } + + public static bool IsOverride(this XElement element) => element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase); + + public static XElement FirstElement(this XElement element) => element.Elements().FirstOrDefault(); + + /// + /// Returns the first child element that matches the name using the provided comparison method. + /// + public static XElement GetChildElement(this XContainer container, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => container.Elements().FirstOrDefault(e => e.Name.ToString().Equals(name, comparisonMethod)); + + /// + /// 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)); } } diff --git a/Barotrauma/BarotraumaShared/Source/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/Source/Sprite/Sprite.cs index 2c137d5fc..9b8d516eb 100644 --- a/Barotrauma/BarotraumaShared/Source/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/Source/Sprite/Sprite.cs @@ -5,6 +5,7 @@ using System.Xml.Linq; using System.Linq; using Barotrauma.Extensions; using System.IO; +using SpriteParams = Barotrauma.RagdollParams.SpriteParams; namespace Barotrauma { @@ -36,7 +37,7 @@ namespace Barotrauma //the size of the drawn sprite, if larger than the source, //the sprite is tiled to fill the target size - public Vector2 size; + public Vector2 size = Vector2.One; public float rotation; @@ -100,6 +101,7 @@ namespace Barotrauma public string Name { get; set; } partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn, bool premultiplyAlpha = true); + partial void CalculateSourceRect(); private static void AddToList(Sprite elem) @@ -128,7 +130,7 @@ namespace Barotrauma { LoadTexture(ref sourceVector, ref shouldReturn, preMultipliedAlpha); } - if (shouldReturn) return; + if (shouldReturn) { return; } sourceRect = new Rectangle((int)sourceVector.X, (int)sourceVector.Y, (int)sourceVector.Z, (int)sourceVector.W); size = SourceElement.GetAttributeVector2("size", Vector2.One); size.X *= sourceRect.Width; @@ -149,7 +151,6 @@ namespace Barotrauma Origin = new Vector2(sourceRect.Width - origin.X, origin.Y); } depth = spriteParams.Depth; - // TODO: size? } public Sprite(string newFile, Vector2 newOrigin, bool preMultiplyAlpha = true) @@ -229,7 +230,7 @@ namespace Barotrauma return; } var doc = XMLExtensions.TryLoadXml(path); - if (doc == null || doc.Root == null) { return; } + if (doc == null) { return; } if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(EntityID)) { return; } var spriteElements = doc.Descendants("sprite").Concat(doc.Descendants("Sprite")); var sourceElements = spriteElements.Where(e => e.GetAttributeString("name", null) == Name); diff --git a/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs index 165e3537b..4675e0194 100644 --- a/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs @@ -234,6 +234,9 @@ namespace Barotrauma case "character": case "Character": return (Operator == OperatorType.Equals) == target is Character; + case "limb": + case "Limb": + return (Operator == OperatorType.Equals) == target is Limb; case "item": case "Item": return (Operator == OperatorType.Equals) == target is Item; diff --git a/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs index c855f91ac..79ee3e828 100644 --- a/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs @@ -126,6 +126,8 @@ namespace Barotrauma public readonly float FireSize; + public readonly float SeverLimbsProbability; + public HashSet TargetIdentifiers { get { return targetIdentifiers; } @@ -198,6 +200,7 @@ namespace Barotrauma DebugConsole.ThrowError("Invalid action type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); } break; + case "targettype": case "target": string[] Flags = attribute.Value.Split(','); foreach (string s in Flags) @@ -218,10 +221,14 @@ namespace Barotrauma case "setvalue": setValue = attribute.GetAttributeBool(false); break; - case "targetnames": - DebugConsole.ThrowError("Error in StatusEffect config (" + parentDebugName + ") - use identifiers or tags to define the targets instead of names."); + case "severlimbs": + case "severlimbsprobability": + SeverLimbsProbability = MathHelper.Clamp(attribute.GetAttributeFloat(0.0f), 0.0f, 1.0f); break; + case "targetnames": + case "targets": case "targetidentifiers": + case "targettags": string[] identifiers = attribute.Value.Split(','); targetIdentifiers = new HashSet(); for (int i = 0; i < identifiers.Length; i++) @@ -275,7 +282,7 @@ namespace Barotrauma explosion = new Explosion(subElement, parentDebugName); break; case "fire": - FireSize = subElement.GetAttributeFloat("size",10.0f); + FireSize = subElement.GetAttributeFloat("size", 10.0f); break; case "use": case "useitem": @@ -473,20 +480,24 @@ namespace Barotrauma if (entity is Item item) { + if (targetIdentifiers.Contains("item")) return true; if (item.HasTag(targetIdentifiers)) return true; if (targetIdentifiers.Any(id => id == item.Prefab.Identifier)) return true; } else if (entity is ItemComponent itemComponent) { + if (targetIdentifiers.Contains("itemcomponent")) return true; if (itemComponent.Item.HasTag(targetIdentifiers)) return true; if (targetIdentifiers.Any(id => id == itemComponent.Item.Prefab.Identifier)) return true; } else if (entity is Structure structure) { + if (targetIdentifiers.Contains("structure")) return true; if (targetIdentifiers.Any(id => id == structure.Prefab.Identifier)) return true; } else if (entity is Character character) { + if (targetIdentifiers.Contains("character")) return true; if (targetIdentifiers.Any(id => id == character.SpeciesName)) return true; } @@ -648,6 +659,7 @@ namespace Barotrauma foreach (Limb limb in character.AnimController.Limbs) { limb.character.DamageLimb(entity.WorldPosition, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } } @@ -656,6 +668,7 @@ namespace Barotrauma { if (limb.character.Removed) { continue; } limb.character.DamageLimb(entity.WorldPosition, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); } } diff --git a/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs index c89a97190..2bdaa08d4 100644 --- a/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs @@ -145,7 +145,7 @@ namespace Barotrauma UnlockAchievement(c, "clowncostume"); } - if (Submarine.MainSub != null && c.Submarine == null && c.ConfigPath == Character.HumanConfigFile) + if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName.Equals(Character.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { float dist = 500 / Physics.DisplayToRealWorldRatio; if (Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition) > @@ -218,7 +218,7 @@ namespace Barotrauma causeOfDeath.Killer == Character.Controlled) { SteamManager.IncrementStat( - character.SpeciesName.ToLowerInvariant() == "human" ? "humanskilled" : "monsterskilled", + character.IsHuman ? "humanskilled" : "monsterskilled", 1); } @@ -229,6 +229,14 @@ namespace Barotrauma { UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName + "indoors"); } + if (character.SpeciesName.EndsWith("boss")) + { + UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("boss", "")); + if (character.CurrentHull != null) + { + UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("boss", "") + "indoors"); + } + } if (character.HasEquippedItem("clownmask") && character.HasEquippedItem("clowncostume") && diff --git a/Barotrauma/BarotraumaShared/Source/TextManager.cs b/Barotrauma/BarotraumaShared/Source/TextManager.cs index 9f821d949..1cc71d273 100644 --- a/Barotrauma/BarotraumaShared/Source/TextManager.cs +++ b/Barotrauma/BarotraumaShared/Source/TextManager.cs @@ -210,7 +210,7 @@ namespace Barotrauma } } - if (formatCapitals != null && !GameMain.Config.Language.Contains("Chinese")) + if (formatCapitals != null && (GameMain.Config == null || !GameMain.Config.Language.Contains("Chinese"))) { for (int i = 0; i < variableTags.Length; i++) { @@ -313,8 +313,8 @@ namespace Barotrauma try { - return string.Format(text, args); - } + return string.Format(text, args); + } catch (FormatException) { string errorMsg = "Failed to format text \"" + text + "\", args: " + string.Join(", ", args); @@ -361,7 +361,7 @@ namespace Barotrauma ); } - static readonly string[] genderPronounVariables = new string[] { + static readonly string[] genderPronounVariables = { "[genderpronoun]", "[genderpronounpossessive]", "[genderpronounreflexive]", @@ -370,7 +370,7 @@ namespace Barotrauma "[Genderpronounreflexive]" }; - static readonly string[] genderPronounMaleValues = new string[] { + static readonly string[] genderPronounMaleValues = { "PronounMaleLowercase", "PronounPossessiveMaleLowercase", "PronounReflexiveMaleLowercase", @@ -379,7 +379,7 @@ namespace Barotrauma "PronounReflexiveMale" }; - static readonly string[] genderPronounFemaleValues = new string[] { + static readonly string[] genderPronounFemaleValues = { "PronounFemaleLowercase", "PronounPossessiveFemaleLowercase", "PronounReflexiveFemaleLowercase", @@ -393,10 +393,29 @@ namespace Barotrauma return FormatServerMessage(message, keys.Concat(genderPronounVariables), values.Concat(gender == Gender.Male ? genderPronounMaleValues : genderPronounFemaleValues)); } - static readonly Regex reReplacedMessage = new Regex(@"^(?[\[\].A-Za-z0-9_]+?)=(?.*)$", RegexOptions.Compiled); + // 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) public static string GetServerMessage(string serverMessage) { if (!textPacks.ContainsKey(Language)) @@ -439,12 +458,29 @@ namespace Barotrauma } else { - var match = reReplacedMessage.Match(messages[i]); string messageVariable = null; - if (match.Success) + var matchFormatted = reFormattedMessage.Match(messages[i]); + if (matchFormatted.Success) { - messageVariable = match.Groups["variable"].ToString(); - messages[i] = match.Groups["message"].ToString(); + 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) diff --git a/Barotrauma/BarotraumaShared/Source/TextPack.cs b/Barotrauma/BarotraumaShared/Source/TextPack.cs index f2373007e..99f13b1ab 100644 --- a/Barotrauma/BarotraumaShared/Source/TextPack.cs +++ b/Barotrauma/BarotraumaShared/Source/TextPack.cs @@ -25,7 +25,7 @@ namespace Barotrauma texts = new Dictionary>(); XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) return; + if (doc == null) { return; } Language = doc.Root.GetAttributeString("language", "Unknown"); TranslatedName = doc.Root.GetAttributeString("translatedname", Language); @@ -93,7 +93,7 @@ namespace Barotrauma Dictionary textCounts = new Dictionary(); XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null || doc.Root == null) return; + if (doc == null) { return; } foreach (XElement subElement in doc.Root.Elements()) { diff --git a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs index f891d4797..52168a7e9 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs @@ -119,6 +119,7 @@ namespace Barotrauma DecompressToDirectory(filePath, TempPath, null); XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); + if (doc == null) { return; } string subPath = Path.Combine(TempPath, doc.Root.GetAttributeString("submarine", "")) + ".sub"; Submarine selectedSub = new Submarine(subPath, ""); @@ -130,6 +131,7 @@ namespace Barotrauma DebugConsole.Log("Loading save file for an existing game session (" + filePath + ")"); DecompressToDirectory(filePath, TempPath, null); XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); + if (doc == null) { return; } gameSession.Load(doc.Root); } @@ -303,12 +305,6 @@ namespace Barotrauma public static Stream DecompressFiletoStream(string fileName) { - if (!File.Exists(fileName)) - { - DebugConsole.ThrowError("File \"" + fileName + " doesn't exist!"); - return null; - } - using (FileStream originalFileStream = new FileStream(fileName, FileMode.Open)) { MemoryStream decompressedFileStream = new MemoryStream(); diff --git a/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs index 0eaa47e1c..4e8c60ad1 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs @@ -140,6 +140,15 @@ namespace Barotrauma return fileName; } + private static 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) + { + if (string.IsNullOrEmpty(str)) { return str; } + return removeBBCodeRegex.Replace(str, ""); + } + public static string LimitString(string str, int maxCharacters) { if (str == null || maxCharacters < 0) return null; @@ -418,5 +427,14 @@ namespace Barotrauma hex.AppendFormat("{0:x2}", b); return hex.ToString(); } + + public static string ConvertAbsoluteToRelativePath(string path) + { + string[] splitted = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + string currentFolder = Environment.CurrentDirectory.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Last(); + // Filter out the current folder -> result is "Content/blaahblaah" or "Mods/blaahblaah" etc. + IEnumerable filtered = splitted.SkipWhile(part => part != currentFolder).Skip(1); + return string.Join("/", filtered); + } } } diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index 6a87d75a5..4b1660c9c 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia_ManualDoorTest.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia_ManualDoorTest.sub deleted file mode 100644 index 75f9fa371..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia_ManualDoorTest.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub b/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub index 20ee3fc14..ca3cec2ce 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub and b/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 92da02cb2..4cb7ac8de 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong_Tutorial.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong_Tutorial.sub deleted file mode 100644 index e52f554ea..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong_Tutorial.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index d09115a11..d3c33c61e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback_LadderTest.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback_LadderTest.sub deleted file mode 100644 index 49cd08b94..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback_LadderTest.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index e80bf3664..b593dd09c 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Nautilus.sub b/Barotrauma/BarotraumaShared/Submarines/Nautilus.sub deleted file mode 100644 index 7aa59dae8..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Nautilus.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index dd685f996..8d87d3706 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/PAX.sub b/Barotrauma/BarotraumaShared/Submarines/PAX.sub deleted file mode 100644 index 13fefe7ba..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/PAX.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 048cc52a7..31e005e3b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora_LadderTest.sub b/Barotrauma/BarotraumaShared/Submarines/Remora_LadderTest.sub deleted file mode 100644 index 6489fcfb9..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora_LadderTest.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 276029127..0fe80c54e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/TutorialOutpost.sub b/Barotrauma/BarotraumaShared/Submarines/TutorialOutpost.sub deleted file mode 100644 index 3e5085c2e..000000000 Binary files a/Barotrauma/BarotraumaShared/Submarines/TutorialOutpost.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index fb6c3ad05..a1691f106 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index 30c08d65e..29820d9b5 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 38ea8fcc2..d056a9c77 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,149 @@ +--------------------------------------------------------------------------------------------------------- +v0.9.4.0 +--------------------------------------------------------------------------------------------------------- + +Monster additions and changes: +- New character: Bonethresher. +- New character: Golden Hammerhead. +- Completely remade the Tigerthresher (new skin, ragdoll, behavior and attacks). +- Reworked Hammerhead (new ragdoll, adjusted behavior and attacks). +- Adjusted the behavior of most characters. +- Medium sized monsters can now more easily trigger impact effects when ramming the submarine. +- Characters are now allowed to move faster than previously (9 -> 15). +- Changed the mouth position calculations and redefined the values for all monsters that have the eating behavior. +- Removed Moloch Baby (Which was just a test. Molochs will be revisited in the next update). +- Better bend deformations on limbs. +- Better feedback about hit impacts on limbs when the character takes damage. +- AI monsters should hit moving targets more easily. +- Added configurable health regeneration, which is applied when the character is eating a corpse. +- Added configurable affliction reductions (bleeding, burn, damage). +- Added an option for the monsters to ignore retaliation when they are attacked. +- Added an option to target the attack on specific limb types. +- Added an option to continue the movement, "following through" the target after the attack. + +Modding improvements: +- Mods can override specific content without having to create a completely new content package. This can +be done by surrounding the element with "" (for example, an Item or a Character +config element you want to replace a vanilla Item/Character with). The overrides work based on identifiers; +for instance, if you configure an item with the identifier "wrench" and surround it with the override +tags, it will replace the vanilla wrench. +- Content package load order: the game will first load the core package, then other content in the order + of selection (proper ui still pending). The load order is saved in the config_player.xml. +- Made AI orders and event manager settings moddable. +- The engine and reactor sound ranges can now be modified in the item XML. +- The husk affliction can now be modified and applied on any character. +- Support for multiple simultaneous husk afflictions based on the same system. +- Support for multiple husk appendages. Made the attachment limb configurable in the affliction definition. +- Additional content package validity checks during startup: make sure all XML files in the package can +be loaded, and disable the package if they can't. Fixes tons of console errors on startup when a mod with +invalid XML files is enabled. +- Don't allow publishing workshop items that contain invalid XML files. +- Don't allow selecting invalid content packages in the settings menu. +- Fixed disabled content packages becoming active when they're autoupdated. + +Character editor improvements: +- Improved layout, hotkeys, general UI/UX improvements. +- Support for editing the character configuration files including health, ai, inventory, sounds, particles etc. +- Support for limb sounds. +- Support for light sources attached to limbs. +- Support for adding and removing attacks. +- Support for afflictions in attacks. +- Support for damage modifiers in limbs. +- New functionality: creating limbs (without having to operate on duplicates). +- Support for creating humanoids. +- Allow to create new characters based on an existing character (copy character). +- Added a limb specific "sprite orientation" that overrides the universal "spritesheet orientation". +- Option for mirroring limb sprites. +- Option for hiding the limb sprite (invisible limbs). +- Multiple minor additions and changes, like an option to enable lights or to display the damage modifiers +drawn on the character. +- Exposed structure sound types. +- Exposed the mouth position. +- Exposed the angular damping and density for limbs (removed mass, which was not used). +- Added a default texture path for ragdolls so that the texture can only be defined once. Limb specific +texture definitions override this. +- Allow to define a group for different species. The characters in the same group are friendly to each other. +- DecorativeSprites can now be used on character limbs (like on items). +- Made it easier to adjust the source rect's position with the arrows. Hold CTRL to change the size. +- Fixed a rare case where characters got stunned in the character editor. + +Misc improvements: +- New husk faces. +- Increased default VOIP and microphone volume and the maximum values. +- Force a restart when switching the language. +- More detailed "x is not a valid XML file" error messages. +- Improved mission and traitor info popups. +- Show the number of lights (total and shadow-casting) in the sub editor. +- When rewiring, clicking the right mouse button only removes one node from the wire. +- Wire nodes are added by left clicking in the sub editor, and can be laid out horizontally/vertically +by holding shift. +- Improved logging for DXGI_ERROR crashes on startup. +- Made level ambient lighting darker. +- Added "pause" console command (only usable in single player). + +Bugfixes: +- Fixed crashing when the recharge speed of a PowerContainer with no interface (e.g. Alien Generator) is +adjusted by a signal or a bot. +- Fixed docking interface becoming active on navigation terminals when any of the submarine's docking +ports are close to another docking port, even if the terminal in question is not wired to that port. +- Fixed an occasional crash when a bot starts retreating away from an enemy immediately after spawning. +- Fixed clients not creating a download prompt when a sub they don't have is selected by vote. +- Fixed traitor missions almost always placing the mission-related items inside the same containers. +- Fixed traitor goal durations being displayed as "duration(xx) seconds" instead of "xx seconds". +- Fixed Traitor's "find an item" objectives not being considered complete if the target item is inside +another item within the traitor's inventory. +- Fixed server using the provided campaign savefile name as-is (without the required .save file extension) +when starting a new campaign through the console. Caused clients to throw "File transfer failed (wrong +file extension """!)" errors and prevented them from receiving the save files. +- Fixed servers being able to start the round multiple times by spamming the "start" console command +before loading the round finishes. +- Fixed rewiring sound playing whenever a remote player is using a rewireable device. +- Fixed subinventories not opening when grabbing another character with no items in the corresponding slot. +- Fixed draggable inventories getting stuck to a half-open state if the item is equipped when +the inventory is opening/closing. +- Fixed light sprites being mirrored when the item is mirrored, even if the mirroring the item's sprite +had been disabled (e.g. junction boxes). +- Fixed motion sensor detection area not being flipped when the item is mirrored. +- Fixed status monitor not mirroring rooms on the display in mirrored subs. +- Fixed engine propeller position not being flipped when the item is mirrored. +- Fixed diving suits being displayed in an incorrect position when the locker has been flipped on either axis. +- AI characters turn autotemp back on when leaving the reactor. +- Fixed item sprites becoming unmirrored when a damaged sprite is fading in. +- Fixed damaged item sprites that are set to fade in according to the damage always being drawn at full opacity. +- Fixed spectators being distributed into teams in combat missions, potentially leading to imbalanced crew sizes. +- Notify the client using the "togglekarmatestmode" command about the test mode being enabled/disabled. +- Send karma change notifications when karma has changed by 1 unit or more when test mode is enabled, not +just when an action causes an immediate change of 1 unit or more. +- Fixed adjacent sprites bleeding into the platform and topwindow sprites. +- Fixed autocompleting submarine/shuttle names when using the submarine/shuttle console commands. +- Fixed some items (like sonar beacon) attracting monsters even when they're powered off. +- Fixed inability to open the pause menu if an inventory slot had been highlighted when exiting +the game screen. +- Fixed welded door sprites "twitching" when the submarine moves. +- Fixed crashing when a character with no hands or arms drops a holdable item. +- Non-humanoids (i.e. monsters controlled by a player) cannot be assigned as traitors. +- Traitor missions are considered unsuccessful if the objectives cannot be completed (for example if the +submarine doesn't have suitable containers to place the traitor items inside). +- Fixed the "Barotrauma" title text staying invisible in the main menu when coming back from the credits. +- Fixed clients not refreshing an item's editing hud when a remote player adjusts the values (i.e. if two +players had selected the same lamp and one changed the color value, the other client wouldn't see +the value change in the editing hud). +- Fixed a couple of the additive light sprites being slightly offset from the lamp. +- Clients don't display client-side vitality changes in healthbars until the actual vitality is received +from the server. Fixes health occasionally dropping and then jumping back up if the client predicts damage +incorrectly (e.g. if a melee attack hits client-side but doesn't server-side). +- Fixed character syncing being very inaccurate when switching to freecam and spectating a character +far away from other players. +- Fixed oxygen/fuel tanks attached to a tool being rendered behind the character's legs. + +--------------------------------------------------------------------------------------------------------- +v0.9.3.3 +--------------------------------------------------------------------------------------------------------- + +- Fixed an error when trying to start a multiplayer campaign with voting enabled +- Fixed a character inventory desyncing error in multiplayer that caused random disconnections +- Fixed download prompts for submarines not showing up + --------------------------------------------------------------------------------------------------------- v0.9.3.2 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/config.xml b/Barotrauma/BarotraumaShared/config.xml index 6e38e3c7c..7821a1e40 100644 --- a/Barotrauma/BarotraumaShared/config.xml +++ b/Barotrauma/BarotraumaShared/config.xml @@ -2,6 +2,7 @@ \ No newline at end of file