v1.12.6.2 (Spring Update 2026)

This commit is contained in:
Regalis11
2026-04-09 15:10:07 +03:00
parent c8657caefa
commit a4607dffad
197 changed files with 5586 additions and 3461 deletions

View File

@@ -29,7 +29,7 @@ namespace Barotrauma
}
}
}
else if (SelectedAiTarget?.Entity != null)
else if (SelectedAiTarget?.Entity != null && AttackLimb != null)
{
Vector2 targetPos = SelectedAiTarget.Entity.DrawPosition;
if (State == AIState.Attack)
@@ -37,15 +37,16 @@ namespace Barotrauma
targetPos = attackWorldPos;
}
targetPos.Y = -targetPos.Y;
GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4);
Vector2 attackLimbPos = AttackLimb.DrawPosition;
attackLimbPos.Y = -attackLimbPos.Y;
GUI.DrawLine(spriteBatch, attackLimbPos, targetPos, GUIStyle.Red * 0.75f, 0, 4);
if (wallTarget != null && !IsCoolDownRunning)
{
Vector2 wallTargetPos = wallTarget.Position;
if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.DrawPosition; }
wallTargetPos.Y = -wallTargetPos.Y;
GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false);
GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5);
GUI.DrawLine(spriteBatch, attackLimbPos, wallTargetPos, Color.Orange * 0.75f, 0, 5);
}
GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black);
GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {CurrentTargetMemory?.Priority.FormatZeroDecimal()}, P: {CurrentTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black);

View File

@@ -23,6 +23,8 @@ namespace Barotrauma
//GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black);
}
Vector2 spacing = new Vector2(0, GUIStyle.Font.MeasureChar('T').Y);
Vector2 stringDrawPos = pos + textOffset;
GUI.DrawString(spriteBatch, stringDrawPos, Character.Name, Color.White, Color.Black);
@@ -33,14 +35,14 @@ namespace Barotrauma
currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
for (int i = 0; i < currentOrders.Count; i++)
{
stringDrawPos += new Vector2(0, 20);
stringDrawPos += spacing;
var order = currentOrders[i];
GUI.DrawString(spriteBatch, stringDrawPos, $"ORDER {i + 1}: {order.Objective.DebugTag} ({order.Objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black);
}
}
else if (ObjectiveManager.WaitTimer > 0)
{
stringDrawPos += new Vector2(0, 20);
stringDrawPos += spacing;
GUI.DrawString(spriteBatch, stringDrawPos - textOffset, $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black);
}
var currentObjective = ObjectiveManager.CurrentObjective;
@@ -49,19 +51,19 @@ namespace Barotrauma
int offset = currentOrder != null ? 20 + ((ObjectiveManager.CurrentOrders.Count - 1) * 20) : 0;
if (currentOrder == null || currentOrder.Priority <= 0)
{
stringDrawPos += new Vector2(0, 20);
stringDrawPos += spacing;
GUI.DrawString(spriteBatch, stringDrawPos, $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black);
}
var subObjective = currentObjective.CurrentSubObjective;
if (subObjective != null)
{
stringDrawPos += new Vector2(0, 20);
stringDrawPos += spacing;
GUI.DrawString(spriteBatch, stringDrawPos, $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black);
}
var activeObjective = ObjectiveManager.GetActiveObjective();
if (activeObjective != null)
{
stringDrawPos += new Vector2(0, 20);
stringDrawPos += spacing;
GUI.DrawString(spriteBatch, stringDrawPos, $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black);
}
if (currentObjective is AIObjectiveCombat
@@ -85,12 +87,12 @@ namespace Barotrauma
}
}
Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, 40);
Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, spacing.Y * 2);
for (int i = 0; i < ObjectiveManager.Objectives.Count; i++)
{
var objective = ObjectiveManager.Objectives[i];
GUI.DrawString(spriteBatch, objectiveStringDrawPos, $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f);
objectiveStringDrawPos += new Vector2(0, 18);
objectiveStringDrawPos += spacing * 0.8f;
}
if (steeringManager is IndoorsSteeringManager pathSteering)

View File

@@ -547,7 +547,7 @@ namespace Barotrauma
}
}
public void Draw(SpriteBatch spriteBatch, Camera cam)
public void Draw(SpriteBatch spriteBatch, Camera cam, bool onlyDrawSeveredLimbs)
{
if (simplePhysicsEnabled) { return; }
@@ -573,8 +573,12 @@ namespace Barotrauma
{
foreach (Limb limb in limbs) { limb.ActiveSprite.Depth += depthOffset; }
}
for (int i = 0; i < limbs.Length; i++)
for (int i = 0; i < inversedLimbDrawOrder.Length; i++)
{
if (onlyDrawSeveredLimbs && !inversedLimbDrawOrder[i].IsSevered)
{
continue;
}
inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color);
}
if (!MathUtils.NearlyEqual(depthOffset, 0.0f))

View File

@@ -938,8 +938,8 @@ namespace Barotrauma
public void Draw(SpriteBatch spriteBatch, Camera cam)
{
if (!Enabled || InvisibleTimer > 0.0f) { return; }
AnimController.Draw(spriteBatch, cam);
if (!Enabled) { return; }
AnimController.Draw(spriteBatch, cam, onlyDrawSeveredLimbs: InvisibleTimer > 0.0f);
}
public void DrawHUD(SpriteBatch spriteBatch, Camera cam, bool drawHealth = true)

View File

@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma;
@@ -106,12 +107,17 @@ public static class InteractionLabelManager
}
RectangleF textRect = GetLabelRect(interactableInRange, cam);
if (labels.None(l => l.Item == interactableInRange))
var existingLabel = labels.FirstOrDefault(l => l.Item == interactableInRange);
if (existingLabel == null)
{
var labelData = new LabelData(interactableInRange, textRect, RichString.Rich(interactableInRange.Prefab.Name), cam);
labels.Add(labelData);
}
//size of the label doesn't match - can happen when we're using a CJK font which we asynchronously render new symbols for
else if (existingLabel.TextRect.Size != textRect.Size)
{
existingLabel.TextRect = textRect;
}
}
PreventInteractionLabelOverlap(centerPos: character.Position);
@@ -127,7 +133,11 @@ public static class InteractionLabelManager
private static RectangleF GetLabelRect(Item item, Camera cam)
{
// create rectangle for overlap prevention
Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(RichString.Rich(item.Prefab.Name).SanitizedValue) * LabelScale;
string nameText = RichString.Rich(item.Prefab.Name).SanitizedValue;
var font = GUIStyle.SubHeadingFont.GetFontForStr(nameText)!;
Vector2 itemTextSizeScreen = font.MeasureString(nameText) * LabelScale;
Vector2 interactablePosScreen = cam.WorldToScreen(item.Position);
RectangleF textRect = new RectangleF(interactablePosScreen.X, interactablePosScreen.Y, itemTextSizeScreen.X, itemTextSizeScreen.Y);
// center the rectangle on the item

View File

@@ -340,10 +340,6 @@ namespace Barotrauma
break;
case "randomcolor":
randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandomUnsynced();
if (randomColor.HasValue)
{
Params.GetSprite().Color = randomColor.Value;
}
break;
case "lightsource":
LightSource = new LightSource(subElement, GetConditionalTarget())
@@ -631,6 +627,8 @@ namespace Barotrauma
SoundPlayer.PlayDamageSound(damageSoundType, Math.Max(damage, bleedingDamage), WorldPosition);
}
if (character.InvisibleTimer > 0.0f) { return; }
// spawn damage particles
float damageParticleAmount = damage < 1 ? 0 : Math.Min(damage / 5, 1.0f) * damageMultiplier;
if (damageParticleAmount > 0.001f)
@@ -734,7 +732,8 @@ namespace Barotrauma
if (spriteParams == null || Alpha <= 0) { return; }
float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength;
float brightness = Math.Max(1.0f - burn, 0.2f);
Color tintedColor = spriteParams.Color;
Color baseColor = randomColor ?? spriteParams.Color;
Color tintedColor = baseColor;
if (!spriteParams.IgnoreTint)
{
tintedColor = tintedColor.Multiply(ragdoll.RagdollParams.Color);
@@ -752,7 +751,7 @@ namespace Barotrauma
}
}
Color color = new Color(tintedColor.Multiply(brightness), tintedColor.A);
Color colorWithoutTint = new Color(spriteParams.Color.Multiply(brightness), spriteParams.Color.A);
Color colorWithoutTint = new Color(baseColor.Multiply(brightness), baseColor.A);
Color blankColor = new Color(brightness, brightness, brightness, 1);
if (deadTimer > 0)
{

View File

@@ -31,7 +31,7 @@ namespace Barotrauma
GUILayoutGroup connLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), labelList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), connLayout.RectTransform), text: conn.Connection.DisplayName, font: GUIStyle.SubHeadingFont);
GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DisplayName.Value);
GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DefaultDisplayName.Value);
box.MaxTextLength = MaxConnectionLabelLength;
textBoxes.Add(conn.Name, box);

View File

@@ -277,6 +277,14 @@ namespace Barotrauma
int selectedOption = (userdata as int?) ?? 0;
if (actionInstance != null)
{
var option = actionInstance.Options[selectedOption];
if (GameMain.Client == null && option.ForceSay)
{
Character.Controlled.ForceSay(
option.ForceSayText.IsNullOrEmpty() ? TextManager.Get(option.Text).Fallback(option.Text) : TextManager.Get(option.ForceSayText).Fallback(option.ForceSayText),
option.ForceSayInRadio,
option.ForceSayRemoveQuotes);
}
actionInstance.selectedOption = selectedOption;
DisableButtons(optionButtons, btn);
btn.ExternalHighlight = true;
@@ -340,7 +348,8 @@ namespace Barotrauma
if (speaker?.Info != null && drawChathead)
{
// chathead
new GUICustomComponent(new RectTransform(new Vector2(0.15f, 0.8f), content.RectTransform), onDraw: (sb, customComponent) =>
int chatHeadWidth = (int)(content.RectTransform.Rect.Width * 0.15f);
new GUICustomComponent(new RectTransform(new Point(chatHeadWidth, chatHeadWidth), content.RectTransform, isFixedSize: true), onDraw: (sb, customComponent) =>
{
speaker.Info.DrawIcon(sb, customComponent.Rect.Center.ToVector2(), customComponent.Rect.Size.ToVector2());
});
@@ -382,7 +391,7 @@ namespace Barotrauma
}
textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16));
content.RectTransform.MinSize = new Point(0, content.Children.Sum(c => c.Rect.Height));
content.RectTransform.MinSize = textContent.RectTransform.MinSize;
// Recalculate the text size as it is scaled up and no longer matching the text height due to the textContent's minSize increasing
textBlock.CalculateHeightFromText();

View File

@@ -61,17 +61,9 @@ namespace Barotrauma
{
Item.ReadSpawnData(msg);
}
if (character.Submarine != null && character.AIController is EnemyAIController enemyAi)
if (character.AIController is EnemyAIController enemyAi && character.Submarine is Submarine ownSub)
{
enemyAi.UnattackableSubmarines.Add(character.Submarine);
if (Submarine.MainSub != null)
{
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
foreach (Submarine sub in Submarine.MainSub.DockedTo)
{
enemyAi.UnattackableSubmarines.Add(sub);
}
}
enemyAi.SetUnattackableSubmarines(ownSub);
}
}
if (characters.Contains(null))

View File

@@ -0,0 +1,8 @@
#nullable enable
namespace Barotrauma;
internal sealed partial class CustomMission : Mission
{
public override bool DisplayAsCompleted => State == SuccessState;
public override bool DisplayAsFailed => State == FailureState;
}

View File

@@ -14,7 +14,7 @@ namespace Barotrauma
private void TryShowRetrievedMessage()
{
if (DetermineCompleted())
if (DetermineCompleted(CampaignMode.TransitionType.None))
{
HandleMessage(ref allRetrievedMessage);
}

View File

@@ -1435,8 +1435,15 @@ namespace Barotrauma
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));
var client = RestFactory.CreateClient(remoteDirectory.ToString());
var request = RestFactory.CreateRequest(remoteFileName);
var response = client.Execute(request);
if (response.ErrorException != null)
{
DebugConsole.AddWarning($"Connection error: Failed to load remote sprite from {url} " +
$"({response.ErrorException.Message}).");
return null;
}
if (response.ResponseStatus != ResponseStatus.Completed) { return null; }
if (response.StatusCode != HttpStatusCode.OK) { return null; }

View File

@@ -26,7 +26,9 @@ namespace Barotrauma
public OnSelectedHandler OnDropped;
private readonly GUIButton button;
private readonly GUIButton button;
public GUIButton Button => button;
private readonly GUIImage icon;
private readonly GUIListBox listBox;

View File

@@ -710,19 +710,24 @@ namespace Barotrauma
if (listBox == pendingList || listBox == crewList)
{
nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height));
nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width);
nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height));
Point size = new Point((int)(0.7f * nameBlock.Rect.Height));
new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false };
size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height);
new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null)
//if the character is already in the crew, only check permissions - reputation doesn't matter for renaming an already-hired bot
bool canRename = listBox == crewList ? HasPermissionToHire : CanHire(characterInfo);
if (canRename)
{
Enabled = CanHire(characterInfo),
ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel),
UserData = characterInfo,
OnClicked = CreateRenamingComponent
};
nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height));
nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width);
nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height));
Point iconSize = new Point((int)(0.7f * nameBlock.Rect.Height));
new GUIImage(new RectTransform(iconSize, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false };
Point buttonSize = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width + (int)(iconSize.X * 1.5f), mainGroup.Rect.Height);
new GUIButton(new RectTransform(buttonSize, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null)
{
ClampMouseRectToParent = false,
ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel),
UserData = characterInfo,
OnClicked = CreateRenamingComponent
};
}
}
//recalculate everything and truncate texts if needed

View File

@@ -487,7 +487,21 @@ namespace Barotrauma.Items.Components
return 0.0f;
}
public virtual bool ShouldDrawHUD(Character character)
public bool ShouldDrawHUD(Character character)
{
if (Character.Controlled?.SelectedItem != null)
{
Controller controller = item.GetComponent<Controller>();
if (controller != null && controller.User == Character.Controlled && controller.HideAllItemComponentHUDs)
{
return false;
}
}
return ShouldDrawHUDComponentSpecific(character);
}
protected virtual bool ShouldDrawHUDComponentSpecific(Character character)
{
return true;
}

View File

@@ -552,9 +552,9 @@ namespace Barotrauma.Items.Components
if (flippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; }
float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth;
if (i < containedSpriteDepths.Length)
if (targetSlotIndex < containedSpriteDepths.Length)
{
containedSpriteDepth = containedSpriteDepths[i];
containedSpriteDepth = containedSpriteDepths[targetSlotIndex];
}
containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f;

View File

@@ -52,10 +52,12 @@ namespace Barotrauma.Items.Components
partial void SetLightSourceTransformProjSpecific()
{
Vector2 offset = Vector2.Zero;
if (LightOffset != Vector2.Zero)
Vector2 offset = LightOffset * item.Scale;
if (offset != Vector2.Zero)
{
offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale;
if (item.FlippedX) { offset.X *= -1; }
if (item.FlippedY) { offset.Y *= -1; }
offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad));
}
if (ParentBody != null)
@@ -101,7 +103,10 @@ namespace Barotrauma.Items.Components
if (Light?.LightSprite == null) { return; }
if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled)
{
Vector2 offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale;
Vector2 offset = LightOffset * item.Scale;
if (item.FlippedX) { offset.X *= -1; }
if (item.FlippedY) { offset.Y *= -1; }
offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad));
Vector2 origin = Light.LightSprite.Origin;
if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; }
@@ -114,6 +119,7 @@ namespace Barotrauma.Items.Components
{
color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value);
}
Light.LightSprite.Draw(spriteBatch,
new Vector2(drawPos.X, -drawPos.Y),
color * lightBrightness,
@@ -128,8 +134,16 @@ namespace Barotrauma.Items.Components
{
if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX)
{
Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ?
SpriteEffects.FlipHorizontally : SpriteEffects.None;
Light.LightSpriteEffect ^= SpriteEffects.FlipHorizontally;
}
SetLightSourceTransformProjSpecific();
}
public override void FlipY(bool relativeToSub)
{
if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipY)
{
Light.LightSpriteEffect ^= SpriteEffects.FlipVertically;
}
SetLightSourceTransformProjSpecific();
}

View File

@@ -8,6 +8,30 @@ namespace Barotrauma.Items.Components
{
private bool isHUDsHidden;
public void UpdateMsg()
{
if (Character.Controlled == null) { return; }
if (!string.IsNullOrEmpty(KickOutCharacterMsg) &&
SelectingKicksCharacterOut &&
User != null && !User.Removed)
{
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(KickOutCharacterMsg));
}
else if (!string.IsNullOrEmpty(PutOtherCharacterMsg) &&
AllowPuttingInOtherCharacters &&
CanPutSelectedCharacter(Character.Controlled.SelectedCharacter))
{
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(PutOtherCharacterMsg));
}
else
{
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg));
}
CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled);
}
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
base.DrawHUD(spriteBatch, character);
@@ -69,21 +93,33 @@ namespace Barotrauma.Items.Components
ushort userID = msg.ReadUInt16();
if (userID == 0)
{
if (user != null)
if (User != null)
{
IsActive = false;
CancelUsing(user);
user = null;
CancelUsing(User);
User = null;
}
}
else
{
Character newUser = Entity.FindEntityByID(userID) as Character;
if (newUser != user)
if (newUser != User)
{
CancelUsing(user);
CancelUsing(User);
}
user = newUser;
User = newUser;
// If the server assigned a user to this controller but the character is not selecting the item
// on the client-side, force the selection to prevent desync. This is required for force attaching,
// since the character placed into the controller may be unconscious, and in that state
// the server no longer syncs the current SelectedItem to clients.
if (ForceUserToStayAttached &&
user != null &&
!user.IsAnySelectedItem(Item))
{
user.SelectedItem = Item;
}
IsActive = true;
}
}

View File

@@ -434,18 +434,13 @@ namespace Barotrauma.Items.Components
foreach (FabricationRecipe fi in fabricationRecipes.Values)
{
RichString recipeTooltip =
fi.RequiresRecipe ?
RichString.Rich(fi.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖") :
RichString.Rich(fi.TargetItem.Description);
var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null)
{
UserData = fi,
HoverColor = Color.Gold * 0.2f,
SelectedColor = Color.Gold * 0.5f,
ToolTip = recipeTooltip
};
SetRecipeTooltip(frame, fi);
var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform),
childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f };
@@ -457,7 +452,7 @@ namespace Barotrauma.Items.Components
itemIcon, scaleToFit: true)
{
Color = itemIcon == fi.TargetItem.Sprite ? fi.TargetItem.SpriteColor : fi.TargetItem.InventoryIconColor,
ToolTip = recipeTooltip
CanBeFocused = false
};
}
@@ -466,7 +461,7 @@ namespace Barotrauma.Items.Components
{
Padding = Vector4.Zero,
AutoScaleVertical = true,
ToolTip = recipeTooltip
CanBeFocused = false
};
new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight),
@@ -478,6 +473,20 @@ namespace Barotrauma.Items.Components
}
}
private void SetRecipeTooltip(GUIComponent component, FabricationRecipe recipe)
{
if (!recipe.RequiresRecipe)
{
component.ToolTip = RichString.Rich(recipe.TargetItem.Description);
}
else
{
component.ToolTip = AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem) ?
RichString.Rich(recipe.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Green)}‖{TextManager.Get("unlockedrecipe.true")}‖color:end‖") :
RichString.Rich(recipe.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖");
}
}
private void InitInventoryUIs()
{
if (inputInventoryHolder != null)
@@ -927,16 +936,24 @@ namespace Barotrauma.Items.Components
}
}
if (recipe.RequiresRecipe && recipe.HideIfNoRecipe)
if (recipe.RequiresRecipe)
{
if (Character.Controlled != null)
if (recipe.HideIfNoRecipe)
{
if (!AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem))
bool anyOneHasRecipe = AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem);
if (Character.Controlled != null)
{
child.Visible = false;
continue;
if (!anyOneHasRecipe)
{
child.Visible = false;
continue;
}
}
}
else
{
SetRecipeTooltip(child, recipe);
}
}
child.Visible =
@@ -1147,7 +1164,16 @@ namespace Barotrauma.Items.Components
var lines = description.WrappedText.Split('\n');
if (lines.Count <= 1) { break; }
string newString = string.Join('\n', lines.Take(lines.Count - 1));
description.Text = newString.Substring(0, newString.Length - 4) + "...";
if (newString.Length > 4)
{
description.Text = newString.Substring(0, newString.Length - 4) + "...";
}
else
{
description.Text = newString + "...";
}
description.CalculateHeightFromText();
description.ToolTip = richDescription;
}

View File

@@ -443,6 +443,7 @@ namespace Barotrauma.Items.Components
var wire = targetItem.GetComponent<Wire>();
if (wire != null && wire.Connections.Any(c => c != null)) { return false; }
if (targetItem.Container is { NonInteractable: true }) { return false; }
if (targetItem.Container?.GetComponent<ItemContainer>() is { DrawInventory: false } or { AllowAccess: false }) { return false; }
if (targetItem.HasTag(Tags.TraitorMissionItem)) { return false; }

View File

@@ -575,6 +575,22 @@ namespace Barotrauma.Items.Components
pos /= c.Resources.Count;
MineralClusters.Add((center: pos, resources: c.Resources));
}
if (GameMain.GameSession != null)
{
foreach (var mission in GameMain.GameSession.Missions)
{
if (mission is MineralMission mineralMission)
{
foreach (var minerals in mineralMission.SpawnedResources)
{
MineralClusters.Add((
center: new Vector2(minerals.Average(m => m.WorldPosition.X), minerals.Average(m => m.WorldPosition.Y)),
resources: minerals));
}
}
}
}
}
else
{
@@ -823,18 +839,20 @@ namespace Barotrauma.Items.Components
if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; }
if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; }
float sonarSoundRange = t.SoundRange * t.SoundRangeOnSonarMultiplier;
float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter);
if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; }
if (distSqr > sonarSoundRange * sonarSoundRange * 2) { continue; }
float dist = (float)Math.Sqrt(distSqr);
if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500)
{
Ping(t.WorldPosition, transducerCenter,
t.SoundRange * DisplayScale, 0, DisplayScale, range,
sonarSoundRange * DisplayScale, 0, DisplayScale, range,
passive: true, pingStrength: 0.5f, needsToBeInSector: t);
if (t.IsWithinSector(transducerCenter))
{
sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(t.SoundRange / 2000, 1.0f, 5.0f)));
sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(sonarSoundRange / 2000, 1.0f, 5.0f)));
}
}
}
@@ -977,7 +995,9 @@ namespace Barotrauma.Items.Components
if (aiTarget.InDetectable) { continue; }
if (aiTarget.SonarLabel.IsNullOrEmpty() || aiTarget.SoundRange <= 0.0f) { continue; }
if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange)
float sonarSoundRange = aiTarget.SoundRange * aiTarget.SoundRangeOnSonarMultiplier;
if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < sonarSoundRange * sonarSoundRange)
{
DrawMarker(spriteBatch,
aiTarget.SonarLabel.Value,

View File

@@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components
get { return Vector2.Zero; }
}
public override bool ShouldDrawHUD(Character character)
protected override bool ShouldDrawHUDComponentSpecific(Character character)
{
if (item.IsHidden) { return false; }
if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; }

View File

@@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components
}
}
public override bool ShouldDrawHUD(Character character)
protected override bool ShouldDrawHUDComponentSpecific(Character character)
=> character == Character.Controlled && (character.SelectedItem == item || character.SelectedSecondaryItem == item);
public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)

View File

@@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components
MoveConnectedWires(amount);
}
public override bool ShouldDrawHUD(Character character)
protected override bool ShouldDrawHUDComponentSpecific(Character character)
{
return character == Character.Controlled && character == user && (character.SelectedItem == item || character.SelectedSecondaryItem == item);
}

View File

@@ -287,20 +287,24 @@ namespace Barotrauma.Items.Components
texts.Add(target.CustomInteractHUDText);
textColors.Add(GUIStyle.Green);
}
if (!target.IsIncapacitated && target.IsPet)
if (equipper?.FocusedCharacter == target)
{
texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use));
textColors.Add(GUIStyle.Green);
}
if (equipper?.FocusedCharacter == target && target.CanBeHealedBy(equipper, checkFriendlyTeam: false))
{
texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health));
textColors.Add(GUIStyle.Green);
}
if (target.CanBeDraggedBy(Character.Controlled))
{
texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab));
textColors.Add(GUIStyle.Green);
if (!target.IsIncapacitated && target.IsPet &&
target.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior.CanPlayWith(Character.Controlled))
{
texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use));
textColors.Add(GUIStyle.Green);
}
if (target.CanBeHealedBy(equipper, checkFriendlyTeam: false))
{
texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health));
textColors.Add(GUIStyle.Green);
}
if (target.CanBeDraggedBy(Character.Controlled))
{
texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab));
textColors.Add(GUIStyle.Green);
}
}
if (target.IsUnconscious)

View File

@@ -1597,7 +1597,8 @@ namespace Barotrauma
{
if (DraggingSlot == null || (!DraggingSlot.MouseOn()))
{
Sprite sprite = DraggingItems.First().Prefab.InventoryIcon ?? DraggingItems.First().Sprite;
Item firstDraggingItem = DraggingItems.First();
Sprite sprite = firstDraggingItem.OverrideInventorySprite ?? firstDraggingItem.Prefab.InventoryIcon ?? firstDraggingItem.Sprite;
int iconSize = (int)(64 * GUI.Scale);
float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f);
@@ -1854,7 +1855,7 @@ namespace Barotrauma
if (item != null && drawItem)
{
Sprite sprite = item.Prefab.InventoryIcon ?? item.Sprite;
Sprite sprite = item.OverrideInventorySprite ?? item.Prefab.InventoryIcon ?? item.Sprite;
float scale = Math.Min(Math.Min((rect.Width - 10) / sprite.size.X, (rect.Height - 10) / sprite.size.Y), 2.0f);
Vector2 itemPos = rect.Center.ToVector2();
if (itemPos.Y > GameMain.GraphicsHeight)

View File

@@ -419,7 +419,7 @@ namespace Barotrauma
if (fadeInBrokenSprite != null)
{
float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
float d = MathHelper.Clamp(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.0f, 0.999f);
fadeInBrokenSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha,
textureScale: Vector2.One * Scale,
depth: d);
@@ -435,7 +435,7 @@ namespace Barotrauma
activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth);
if (fadeInBrokenSprite != null)
{
float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f);
float d = MathHelper.Clamp(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.0f, 0.999f);
fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, RotationRad, Scale, activeSprite.effects, d);
}
}
@@ -885,7 +885,12 @@ namespace Barotrauma
Spacing = (int)(25 * GUI.Scale)
};
var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this };
var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true,
titleFont: GUIStyle.LargeFont,
dimOutDefaultValues: false)
{
UserData = this
};
activeEditors.Add(itemEditor);
itemEditor.Children.First().Color = Color.Black * 0.7f;
if (!inGame)
@@ -1045,7 +1050,12 @@ namespace Barotrauma
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine");
var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUIStyle.SubHeadingFont) { UserData = ic };
var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame,
titleFont: GUIStyle.SubHeadingFont,
dimOutDefaultValues: false)
{
UserData = ic
};
componentEditor.Children.First().Color = Color.Black * 0.7f;
activeEditors.Add(componentEditor);
@@ -1064,7 +1074,12 @@ namespace Barotrauma
requiredItems.Add(relatedItem);
}
}
requiredItems.AddRange(ic.DisabledRequiredItems);
//if we have some actual requirements, no need to keep the empty requirement
//as a "placeholder" for the user to add requirements in the sub editor
if (ic.RequiredItems.None())
{
requiredItems.AddRange(ic.DisabledRequiredItems);
}
foreach (RelatedItem relatedItem in requiredItems)
{
@@ -1626,12 +1641,16 @@ namespace Barotrauma
activeComponents.Clear();
activeComponents.AddRange(components);
foreach (MapEntity entity in linkedTo)
Controller controller = GetComponent<Controller>();
if (controller == null || controller.User != Character.Controlled || !controller.HideAllItemComponentHUDs)
{
if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i)
foreach (MapEntity entity in linkedTo)
{
if (!i.DisplaySideBySideWhenLinked) { continue; }
activeComponents.AddRange(i.components);
if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i)
{
if (!i.DisplaySideBySideWhenLinked) { continue; }
activeComponents.AddRange(i.components);
}
}
}
@@ -1701,7 +1720,9 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter != character &&
otherCharacter.SelectedItem == this)
otherCharacter.SelectedItem == this &&
// Prevent the in use message from being shown if a character is, for example, inside the deconstructor
!otherCharacter.IsAttachedToController())
{
ItemInUseWarning.Visible = true;
if (mergedHUDRect.Width > GameMain.GraphicsWidth / 2) { mergedHUDRect.Inflate(-GameMain.GraphicsWidth / 4, 0); }
@@ -1751,6 +1772,11 @@ namespace Barotrauma
}
}
public void ClearActiveHUDs()
{
activeHUDs.Clear();
}
readonly List<ColoredText> texts = new();
public List<ColoredText> GetHUDTexts(Character character, bool recreateHudTexts = true)
{

View File

@@ -166,6 +166,14 @@ namespace Barotrauma
subElement.GetAttributeBool("fadein", false),
subElement.GetAttributePoint("offset", Point.Zero));
if (brokenSprite.FadeIn && brokenSprite.MaxConditionPercentage <= 0.0f)
{
DebugConsole.AddWarning(
$"Potential error in item {Identifier}: a broken sprite that's set to fade in despite the max condition being 0."+
" The sprite cannot fade in if it's set to only appear when the item is fully broken.",
ContentPackage);
}
int spriteIndex = 0;
for (int i = 0; i < brokenSprites.Count && brokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++)
{

View File

@@ -16,15 +16,17 @@ namespace Barotrauma
{
public readonly UInt32 DecalId;
public readonly int SpriteIndex;
public Vector2 NormalizedPos;
public readonly Vector2 NormalizedPos;
public readonly float Scale;
public readonly float DecalAlpha;
public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale)
public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale, float decalAlpha)
{
DecalId = decalId;
SpriteIndex = spriteIndex;
NormalizedPos = normalizedPos;
Scale = scale;
DecalAlpha = decalAlpha;
}
}
@@ -696,7 +698,7 @@ namespace Barotrauma
var decal = decalEventData.Decal;
int decalIndex = decals.IndexOf(decal);
msg.WriteByte((byte)(decalIndex < 0 ? 255 : decalIndex));
msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8);
msg.WriteRangedSingle(decal.BaseAlpha, 0f, 1f, 8);
break;
default:
throw new Exception($"Malformed hull event: did not expect {eventData.GetType().Name}");
@@ -752,7 +754,9 @@ namespace Barotrauma
float normalizedXPos = msg.ReadRangedSingle(0.0f, 1.0f, 8);
float normalizedYPos = msg.ReadRangedSingle(0.0f, 1.0f, 8);
float decalScale = msg.ReadRangedSingle(0.0f, 2.0f, 12);
remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale));
float decalAlpha = msg.ReadRangedSingle(0f, 1f, 8);
remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale, decalAlpha));
}
break;
case EventType.BallastFlora:
@@ -804,7 +808,8 @@ namespace Barotrauma
decalPosX += Submarine.Position.X;
decalPosY += Submarine.Position.Y;
}
AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex);
Decal decal = AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex);
decal.BaseAlpha = remoteDecal.DecalAlpha;
}
remoteDecals.Clear();
}

View File

@@ -296,13 +296,9 @@ namespace Barotrauma.Lights
light.Priority = lightPriority(range, light);
int i = 0;
while (i < activeLights.Count && light.Priority < activeLights[i].Priority)
{
i++;
}
activeLights.Insert(i, light);
activeLights.Add(light);
}
activeLights.Sort(static (a, b) => b.Priority.CompareTo(a.Priority));
ActiveLightCount = activeLights.Count;
float lightPriority(float range, LightSource light)
@@ -332,7 +328,7 @@ namespace Barotrauma.Lights
activeLights.Remove(activeShadowCastingLights[i]);
}
}
activeLights.Sort((l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime));
activeLights.Sort(static (l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime));
//draw light sprites attached to characters
//render into a separate rendertarget using alpha blending (instead of on top of everything else with alpha blending)

View File

@@ -50,8 +50,14 @@ namespace Barotrauma.Lights
[Serialize("0, 0", IsPropertySaveable.Yes), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)]
public Vector2 Offset { get; set; }
public float RotationRad { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)]
public float Rotation { get; set; }
public float Rotation
{
get => MathHelper.ToDegrees(RotationRad);
set => RotationRad = MathHelper.ToRadians(value);
}
[Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+
" Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")]
@@ -314,6 +320,10 @@ namespace Barotrauma.Lights
private float prevCalculatedRotation;
private float rotation;
/// <summary>
/// Current rotation in radians. Note that LightSourceParams.RotationRad also affects the final rotation of the light.
/// </summary>
public float Rotation
{
get { return rotation; }
@@ -322,7 +332,7 @@ namespace Barotrauma.Lights
if (Math.Abs(value - rotation) < 0.001f) { return; }
rotation = value;
dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation));
RefreshDirection();
if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null)
{
@@ -486,6 +496,9 @@ namespace Barotrauma.Lights
break;
}
}
//make sure the rotation defined in the parameters is taken into account
RefreshDirection();
NeedsRecalculation = true;
}
public LightSource(LightSourceParams lightSourceParams)
@@ -497,6 +510,9 @@ namespace Barotrauma.Lights
{
DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true);
}
//make sure the rotation defined in the parameters is taken into account
RefreshDirection();
NeedsRecalculation = true;
}
public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true)
@@ -511,6 +527,14 @@ namespace Barotrauma.Lights
if (addLight) { GameMain.LightManager.AddLight(this); }
}
/// <summary>
/// Refresh the direction vector of the light (which is used for calculating shadows) based on the rotation and <see cref="LightSourceParams.RotationRad"/>
/// </summary>
private void RefreshDirection()
{
dir = new Vector2(MathF.Cos(rotation - LightSourceParams.RotationRad), -MathF.Sin(rotation - LightSourceParams.RotationRad));
}
public void Update(float time)
{
float brightness = 1.0f;
@@ -773,9 +797,6 @@ namespace Barotrauma.Lights
float boundsExtended = TextureRange;
if (OverrideLightTexture != null)
{
float cosAngle = (float)Math.Cos(rotation);
float sinAngle = -(float)Math.Sin(rotation);
var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
Vector2 origin = OverrideLightTextureOrigin;
@@ -790,8 +811,11 @@ namespace Barotrauma.Lights
origin *= TextureRange;
drawOffset.X = -origin.X * cosAngle - origin.Y * sinAngle;
drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle;
//rotate the origin based on the direction
float cos = dir.X;
float sin = dir.Y;
drawOffset.X = -origin.X * cos - origin.Y * sin;
drawOffset.Y = origin.X * sin + origin.Y * cos;
}
//add a square-shaped boundary to make sure we've got something to construct the triangles from
@@ -1536,7 +1560,6 @@ namespace Barotrauma.Lights
Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition;
lightEffect.World =
Matrix.CreateTranslation(-new Vector3(position, 0.0f)) *
Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) *
Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) *
transform;

View File

@@ -193,7 +193,12 @@ namespace Barotrauma
{
CanTakeKeyBoardFocus = false
};
var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this };
var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true,
titleFont: GUIStyle.LargeFont,
dimOutDefaultValues: false)
{
UserData = this
};
if (editor.Fields.TryGetValue(nameof(Scale).ToIdentifier(), out GUIComponent[] scaleFields) &&
scaleFields.FirstOrDefault() is GUINumberInput scaleInput)

View File

@@ -30,6 +30,13 @@ namespace Barotrauma.Networking
{
DualStack = GameSettings.CurrentConfig.UseDualModeSockets
};
if (NetConfig.UseLenientHandshake)
{
// More lenient timeouts for local testing, so the server would start even without perfect conditions
netPeerConfiguration.ConnectionTimeout = 60.0f;
netPeerConfiguration.ResendHandshakeInterval = 5.0f;
netPeerConfiguration.MaximumHandshakeAttempts = 20;
}
if (endpoint.NetEndpoint.Address.AddressFamily == AddressFamily.InterNetworkV6)
{
netPeerConfiguration.LocalAddress = System.Net.IPAddress.IPv6Any;

View File

@@ -351,7 +351,7 @@ namespace Barotrauma
{
Level.Loaded.DrawBack(graphics, spriteBatch, cam);
}
else if (GameMain.GameSession.GameMode is TestGameMode testMode)
else if (GameMain.GameSession?.GameMode is TestGameMode testMode)
{
graphics.Clear(testMode.BackgroundParams.BackgroundColor);

View File

@@ -49,6 +49,9 @@ namespace Barotrauma
private GUITextBox serverNameBox, passwordBox, maxPlayersBox;
private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox;
private GUIDropDown languageDropdown, serverExecutableDropdown;
#if DEBUG
private GUITickBox lenientHandshakeBox;
#endif
private readonly GUIButton joinServerButton, hostServerButton;
private readonly GUIFrame modsButtonContainer;
@@ -1075,7 +1078,7 @@ namespace Barotrauma
"-public", isPublicBox.Selected.ToString(),
"-playstyle", ((PlayStyle)playstyleBanner.UserData).ToString(),
"-banafterwrongpassword", wrongPasswordBanBox.Selected.ToString(),
"-karmaenabled", (!karmaBox.Selected).ToString(),
"-karmaenabled", (karmaBox.Selected).ToString(),
"-maxplayers", maxPlayersBox.Text,
"-language", languageDropdown.SelectedData.ToString()
};
@@ -1114,6 +1117,13 @@ namespace Barotrauma
int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1);
arguments.Add("-ownerkey");
arguments.Add(ownerKey.ToString());
#if DEBUG
if (lenientHandshakeBox.Selected)
{
arguments.Add("-lenienthandshake");
NetConfig.UseLenientHandshake = true;
}
#endif
var processInfo = new ProcessStartInfo
{
@@ -1368,7 +1378,7 @@ namespace Barotrauma
}
int maxPlayers = Math.Clamp(maxPlayersElement, min: 1, max: NetConfig.MaxPlayers);
var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", true);
var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", false);
var selectedPlayStyle = serverSettings.GetAttributeEnum("playstyle", PlayStyle.Casual);
Vector2 textLabelSize = new Vector2(1.0f, 0.05f);
@@ -1579,10 +1589,18 @@ namespace Barotrauma
karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), TextManager.Get("HostServerKarmaSetting"))
{
Selected = !karmaEnabled,
Selected = karmaEnabled,
ToolTip = TextManager.Get("hostserverkarmasettingtooltip")
};
#if DEBUG
lenientHandshakeBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), "DEBUG: Lenient server startup timeouts")
{
Selected = true,
ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load."
};
#endif
tickboxAreaLower.RectTransform.IsFixedSize = true;
//spacing
@@ -1671,8 +1689,8 @@ namespace Barotrauma
if (string.IsNullOrEmpty(remoteContentUrl)) { return; }
try
{
var client = new RestClient(remoteContentUrl);
var request = new RestRequest("MenuContent.xml", Method.GET);
var client = RestFactory.CreateClient(remoteContentUrl);
var request = RestFactory.CreateRequest("MenuContent.xml");
TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request),
RemoteContentReceived);
}
@@ -1693,12 +1711,17 @@ namespace Barotrauma
try
{
if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); }
if (remoteContentResponse.ErrorException != null)
{
DebugConsole.AddWarning($"Connection error: Failed to fetch remote main menu content " +
$"({remoteContentResponse.ErrorException.Message}).");
return;
}
if (remoteContentResponse.StatusCode != HttpStatusCode.OK)
{
DebugConsole.AddWarning(
"Failed to receive remote main menu content. " +
"There may be an issue with your internet connection, or the master server might be temporarily unavailable " +
$"(error code: {remoteContentResponse.StatusCode})");
$"The master server might be temporarily unavailable (HTTP error: {remoteContentResponse.StatusCode})");
return;
}
string xml = remoteContentResponse.Content;

View File

@@ -398,7 +398,7 @@ namespace Barotrauma
string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase);
SaveUtil.DecompressToDirectory(path, dir);
var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName));
var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform());
if (!result.TryUnwrapSuccess(out var newPackage))
{

View File

@@ -733,6 +733,8 @@ namespace Barotrauma
AutoHideScrollBar = false,
OnSelected = (component, userdata) =>
{
//if we're clicking on a checkbox (toggle visibility) on the list, don't select the entry on the list
if (GUI.MouseOn is GUITickBox) { return false; }
//toggling selection is not how listboxes normally work, need to do that manually here
SoundPlayer.PlayUISound(GUISoundType.Select);
if (layerList.SelectedData == userdata)
@@ -3253,6 +3255,20 @@ namespace Barotrauma
= new GUITextBox(new RectTransform((1.0f, 0.15f), saveInPackageLayout.RectTransform),
createClearButton: true);
packToSaveInFilter.OnTextChanged += (GUITextBox textBox, string text) =>
{
foreach (GUIComponent child in packageToSaveInList.Content.Children)
{
child.Visible =
// Get the pkgText from below
!(child.GetChild<GUILayoutGroup>()?.GetChild<GUITextBlock>() is GUITextBlock textBlock &&
!textBlock.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase));
}
return true;
};
GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPackage p)
{
var listItem = new GUIFrame(new RectTransform((1.0f, 0.15f), packageToSaveInList.Content.RectTransform),
@@ -3273,28 +3289,26 @@ namespace Barotrauma
return retVal;
}
ContentPackage ownerPkg = null;
#if DEBUG
//this is a debug-only option so I won't bother submitting it for localization
var modifyVanillaListItem = addItemToPackageToSaveList("Modify Vanilla content package", ContentPackageManager.VanillaCorePackage);
var modifyVanillaListIcon = modifyVanillaListItem.GetChild<GUIFrame>();
GUIStyle.Apply(modifyVanillaListIcon, "WorkshopMenu.EditButton");
if (MainSub?.Info != null && IsVanillaSub(MainSub.Info))
{
ownerPkg = ContentPackageManager.VanillaCorePackage;
}
#endif
var newPackageListItem = addItemToPackageToSaveList(TextManager.Get("CreateNewLocalPackage"), null);
var newPackageListIcon = newPackageListItem.GetChild<GUIFrame>();
var newPackageListText = newPackageListItem.GetChild<GUITextBlock>();
GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon");
new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform),
onUpdate: (f, component) =>
{
foreach (GUIComponent contentChild in packageToSaveInList.Content.Children)
{
contentChild.Visible &= !(contentChild.GetChild<GUILayoutGroup>()?.GetChild<GUITextBlock>() is GUITextBlock tb &&
!tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase));
}
});
ContentPackage ownerPkg = null;
if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); }
if (ownerPkg == null && MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); }
foreach (var p in ContentPackageManager.LocalPackages)
{
var packageListItem = addItemToPackageToSaveList(p.Name, p);
@@ -3849,6 +3863,10 @@ namespace Barotrauma
return true;
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), deleteButtonHolder.RectTransform), TextManager.Get("DragAndDropSubmarineTip").Fallback(LocalizedString.EmptyString), textAlignment: Alignment.Center, font: GUIStyle.Font)
{
Wrap = true
};
if (AutoSaveInfo?.Root != null)
{
@@ -4486,6 +4504,7 @@ namespace Barotrauma
public void ReconstructLayers()
{
Dictionary<string, LayerData> previousLayers = Layers.ToDictionary();
ClearLayers();
foreach (MapEntity entity in MapEntity.MapEntityList)
{
@@ -4494,6 +4513,13 @@ namespace Barotrauma
Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
}
}
foreach ((string layerName, LayerData data) in previousLayers)
{
if (Layers.ContainsKey(layerName))
{
Layers[layerName] = data;
}
}
UpdateLayerPanel();
}

View File

@@ -26,6 +26,8 @@ namespace Barotrauma
public static DateTime NextCommandPush;
public static Tuple<SerializableProperty, PropertyCommand> CommandBuffer;
private bool dimOutDefaultValues;
private bool isReadonly;
public bool Readonly
{
@@ -316,16 +318,17 @@ namespace Barotrauma
}
}
public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null)
public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null, bool dimOutDefaultValues = true)
: this(parent, entity, inGame ?
SerializableProperty.GetProperties<InGameEditable>(entity).Union(SerializableProperty.GetProperties<ConditionallyEditable>(entity).Where(p => p.GetAttribute<ConditionallyEditable>()?.IsEditable(entity) ?? false))
: SerializableProperty.GetProperties<Editable>(entity).Where(p => p.GetAttribute<ConditionallyEditable>()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont)
: SerializableProperty.GetProperties<Editable>(entity).Where(p => p.GetAttribute<ConditionallyEditable>()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont, dimOutDefaultValues)
{
}
public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable<SerializableProperty> properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null)
public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable<SerializableProperty> properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null, bool dimOutDefaultValues = true)
: base(style, new RectTransform(Vector2.One, parent))
{
this.dimOutDefaultValues = dimOutDefaultValues;
elementHeight = (int)(elementHeight * GUI.Scale);
var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox");
var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox");
@@ -523,9 +526,67 @@ namespace Barotrauma
{
propertyField = CreateStringField(entity, property, value.ToString(), displayName, toolTip);
}
if (propertyField != null && dimOutDefaultValues)
{
UpdateTextColors(property, entity, propertyField);
}
return propertyField;
}
private void UpdateTextColors(SerializableProperty property, object parentObject, GUIComponent parentElement)
{
if (!dimOutDefaultValues) { return; }
bool isSetToDefaultValue = false;
object currentValue = property.GetValue(parentObject);
foreach (var attribute in property.Attributes.OfType<Serialize>())
{
if (XMLExtensions.DefaultValueEquals(attribute.DefaultValue, currentValue) ||
//treat null and empty strings as identical, because there's no way to differentiate between those in the editor
(currentValue == null && attribute.DefaultValue is string defaultValueStr && defaultValueStr.IsNullOrEmpty()))
{
isSetToDefaultValue = true;
break;
}
}
foreach (var component in parentElement.GetAllChildren())
{
UpdateTextColors(component, isSetToDefaultValue);
}
}
private void UpdateTextColors(GUIComponent component, bool isSetToDefaultValue)
{
if (!dimOutDefaultValues) { return; }
if (component is GUINumberInput numberInput)
{
SetTextColor(numberInput.TextBox.TextBlock);
}
else if (component is GUIDropDown dropDown)
{
SetTextColor(dropDown.Button.TextBlock);
}
else if (component is GUITextBox textBox)
{
SetTextColor(textBox.TextBlock);
}
else if (component is GUITextBlock textBlock)
{
SetTextColor(textBlock);
}
else if (component is GUITickBox tickBox)
{
SetTextColor(tickBox.TextBlock);
}
void SetTextColor(GUITextBlock textBlock)
{
textBlock.TextColor = new Color(textBlock.TextColor, alpha: isSetToDefaultValue ? 0.5f : 1.0f);
}
}
public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, LocalizedString displayName, LocalizedString toolTip)
{
var editableAttribute = property.GetAttribute<Editable>();
@@ -564,6 +625,7 @@ namespace Barotrauma
tickBox.Selected = propertyValue;
tickBox.Flash(Color.Red);
}
UpdateTextColors(property, entity, tickBox);
return true;
}
};
@@ -611,6 +673,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
};
refresh += () =>
{
@@ -654,6 +717,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
};
HandleSetterValueTampering(numberInput, () => property.GetFloatValue(entity));
@@ -711,6 +775,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
return true;
};
refresh += () =>
@@ -829,6 +894,7 @@ namespace Barotrauma
TrySendNetworkUpdate(entity, property);
textBox.Text = StripPrefabTags(property.GetValue(entity).ToString());
textBox.Flash(GUIStyle.Green, flashDuration: 1f);
UpdateTextColors(property, entity, frame);
}
//restore the entities that were selected before applying
MapEntity.SelectedList.Clear();
@@ -973,6 +1039,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
};
fields[i] = numberInput;
}
@@ -1046,6 +1113,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
};
HandleSetterValueTampering(numberInput, () =>
{
@@ -1126,6 +1194,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
};
fields[i] = numberInput;
}
@@ -1206,6 +1275,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
};
fields[i] = numberInput;
}
@@ -1299,6 +1369,7 @@ namespace Barotrauma
TrySendNetworkUpdate(entity, property);
colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = newVal;
}
UpdateTextColors(property, entity, frame);
};
colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = (Color)property.GetValue(entity);
fields[i] = numberInput;
@@ -1373,6 +1444,7 @@ namespace Barotrauma
{
TrySendNetworkUpdate(entity, property);
}
UpdateTextColors(property, entity, frame);
};
fields[i] = numberInput;
}
@@ -1437,6 +1509,7 @@ namespace Barotrauma
TrySendNetworkUpdate(entity, property);
textBox.Flash(color: GUIStyle.Green, flashDuration: 1f);
}
UpdateTextColors(property, entity, frame);
}
else
{

View File

@@ -387,13 +387,11 @@ These will hide all servers that have a discord.gg link in their name or descrip
try
{
var client = new RestClient($"{remoteContentUrl}spamfilter")
{
CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore)
};
var client = RestFactory.CreateClient($"{remoteContentUrl}spamfilter");
client.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);
client.AddDefaultHeader("Cache-Control", "no-cache");
client.AddDefaultHeader("Pragma", "no-cache");
var request = new RestRequest("serve_spamlist.php", Method.GET);
var request = RestFactory.CreateRequest("serve_spamlist.php");
TaskPool.Add("RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived);
}
catch (Exception e)
@@ -410,12 +408,18 @@ These will hide all servers that have a discord.gg link in their name or descrip
try
{
if (!t.TryGetResult(out IRestResponse? remoteContentResponse)) { throw new Exception("Task did not return a valid result"); }
if (remoteContentResponse.ErrorException != null)
{
DebugConsole.AddWarning(
"Connection error: Failed to receive global spam filter " +
$"({remoteContentResponse.ErrorException.Message}).");
return;
}
if (remoteContentResponse.StatusCode != HttpStatusCode.OK)
{
DebugConsole.AddWarning(
"Failed to receive global spam filter." +
"There may be an issue with your internet connection, or the master server might be temporarily unavailable " +
$"(error code: {remoteContentResponse.StatusCode})");
"Failed to receive global spam filter. " +
$"The master server might be temporarily unavailable, HTTP status: {remoteContentResponse.StatusCode}");
return;
}
string data = remoteContentResponse.Content;

View File

@@ -23,13 +23,27 @@ namespace Barotrauma.Steam
"submarine",
"item",
"monster",
"art",
"mission",
"outpost",
"beaconstation",
"wreck",
"ruin",
"weapons",
"medical",
"equipment",
"art",
"event set",
"total conversion",
"gamemode",
"gameplaymechanics",
"environment",
"item assembly",
"language",
"qol",
"clientside",
"serverside",
"outdated",
"library"
}.ToIdentifiers().ToImmutableArray();
public class ItemThumbnail : IDisposable
@@ -113,10 +127,14 @@ namespace Barotrauma.Steam
string? thumbnailUrl = item.PreviewImageUrl;
if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; }
var client = new RestClient(thumbnailUrl);
var request = new RestRequest(".", Method.GET);
var client = RestFactory.CreateClient(thumbnailUrl);
var request = RestFactory.CreateRequest(".");
IRestResponse response = await client.ExecuteAsync(request, cancellationToken);
if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed })
if (response.ErrorException != null)
{
DebugConsole.NewMessage($"Connection error: Failed to load workshop item thumbnail for {item.Id} ({response.ErrorException.Message}).");
}
else if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed })
{
using var dataStream = new System.IO.MemoryStream();
await dataStream.WriteAsync(response.RawBytes, cancellationToken);

View File

@@ -535,9 +535,9 @@ namespace Barotrauma.Steam
= new GUIListBox(rectT, style: null, isHorizontal: false)
{
UseGridLayout = true,
ScrollBarEnabled = false,
ScrollBarEnabled = true,
ScrollBarVisible = false,
HideChildrenOutsideFrame = false,
HideChildrenOutsideFrame = true,
Spacing = GUI.IntScale(4)
};
tagsList.Content.ClampMouseRectToParent = false;

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.11.5.0</Version>
<Version>1.12.6.2</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.11.5.0</Version>
<Version>1.12.6.2</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.11.5.0</Version>
<Version>1.12.6.2</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.11.5.0</Version>
<Version>1.12.6.2</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.11.5.0</Version>
<Version>1.12.6.2</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -79,6 +79,15 @@ namespace Barotrauma
convAction.SelectedOption = selectedOption;
if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption))
{
var option = convAction.Options[selectedOption];
if (option.ForceSay && sender.Character != null)
{
sender.Character.ForceSay(
option.ForceSayText.IsNullOrEmpty() ? TextManager.Get(option.Text).Fallback(option.Text) : TextManager.Get(option.ForceSayText).Fallback(option.ForceSayText),
option.ForceSayInRadio,
option.ForceSayRemoveQuotes);
}
foreach (Client c in convAction.TargetClients)
{
if (c == sender) { continue; }

View File

@@ -133,7 +133,7 @@ namespace Barotrauma
switch (winCondition)
{
case WinCondition.LastManStanding:
if (crews[0].Count == 0 || crews[1].Count == 0)
if (crews[0].Count == 0 && crews[1].Count == 0)
{
//if there are no characters in either crew, end the round
teamDead[0] = teamDead[1] = true;

View File

@@ -230,6 +230,9 @@ namespace Barotrauma
//handled in TryStartChildServerRelay
i += 2;
break;
case "-lenienthandshake":
NetConfig.UseLenientHandshake = true;
break;
}
}

View File

@@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.WriteBoolean(State);
msg.WriteUInt16(user == null ? (ushort)0 : user.ID);
msg.WriteUInt16(User == null || User.Removed ? (ushort)0 : User.ID);
}
}
}

View File

@@ -16,8 +16,11 @@ namespace Barotrauma.Items.Components
public void ServerEventRead(IReadMessage msg, Client c)
{
if (c.Character == null) { return; }
var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2);
var QTESuccess = msg.ReadBoolean();
FixActions requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2);
bool QTESuccess = msg.ReadBoolean();
if (!item.CanClientAccess(c) || !HasRequiredItems(c.Character, addMessage: false)) { return; }
if (requestedFixAction != FixActions.None)
{
if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage)

View File

@@ -121,9 +121,9 @@ namespace Barotrauma
if (shouldBeRemoved)
{
bool itemAccessDenied = prevItems.Contains(item) && // if the item was in the inventory before
!itemAccessibility[item] && // and the sender is not allowed to access it
(item.PreviousParentInventory == null || // and either the item has no previous inventory
!sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory
!itemAccessibility[item] && // and the sender is not allowed to access it
(item.PreviousParentInventory == null || // and either the item has no previous inventory
!sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory
if (itemAccessDenied)
{
@@ -136,7 +136,7 @@ namespace Barotrauma
Item droppedItem = item;
Entity prevOwner = Owner;
Inventory previousInventory = droppedItem.ParentInventory;
droppedItem.Drop(null);
droppedItem.Drop(sender.Character);
droppedItem.PreviousParentInventory = previousInventory;
var previousCharacterInventory = prevOwner switch
@@ -188,9 +188,18 @@ namespace Barotrauma
if (holdable != null && !holdable.CanBeDeattached()) { continue; }
bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] &&
(sender.Character == null || item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory));
bool itemAccessDenied = !prevItems.Contains(item) &&
!itemAccessibility[item] &&
(item.PreviousParentInventory == null ||
!sender.Character.CanAccessInventory(item.PreviousParentInventory));
// Prevent modified clients from being able to steal items from characters by item swapping with an existing item
// due to drag and drop being enabled
if (!sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets) && GetItemAt(slotIndex) != null)
{
itemAccessDenied = true;
}
//more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed
if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied)
{

View File

@@ -12,8 +12,6 @@ namespace Barotrauma
{
private CoroutineHandle logPropertyChangeCoroutine;
public Inventory PreviousParentInventory;
public override Sprite Sprite
{
get { return base.Prefab?.Sprite; }

View File

@@ -112,6 +112,7 @@ namespace Barotrauma
msg.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8);
msg.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8);
msg.WriteRangedSingle(decal.Scale, 0f, 2f, 12);
msg.WriteRangedSingle(decal.BaseAlpha, 0f, 1f, 8);
}
break;
case BallastFloraEventData ballastFloraEventData:
@@ -251,7 +252,7 @@ namespace Barotrauma
break;
case EventType.Decal:
byte decalIndex = msg.ReadByte();
float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255);
float decalAlpha = msg.ReadRangedSingle(0f, 1f, 8);
if (decalIndex < 0 || decalIndex >= decals.Count) { return; }
if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent<Sprayer>() != null))
{

View File

@@ -66,6 +66,9 @@ namespace Barotrauma.Networking
txt = msg.ReadString() ?? "";
}
// Sanitize incoming text message from client so they can't use RichString features
txt = txt.Replace('‖', ' ');
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; }
c.LastSentChatMsgID = ID;

View File

@@ -244,6 +244,8 @@ namespace Barotrauma
Character targetCharacter = inventory.Owner as Character;
if (yoinker == null || item == null || thiefCharacter == null || targetCharacter == null || thiefCharacter == targetCharacter) { return; }
if (thiefCharacter.TeamID != targetCharacter.TeamID) { return; }
if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; }
@@ -261,7 +263,7 @@ namespace Barotrauma
}
Item foundItem = null;
if (isValid(item))
if (IsValid(item))
{
foundItem = item;
}
@@ -269,7 +271,7 @@ namespace Barotrauma
{
foreach (Item containedItem in item.ContainedItems)
{
if (isValid(containedItem))
if (IsValid(containedItem))
{
foundItem = containedItem;
break;
@@ -277,16 +279,19 @@ namespace Barotrauma
}
}
static bool isValid(Item item)
static bool IsValid(Item item)
{
return item.GetComponent<IdCard>() != null || item.GetComponent<RangedWeapon>() != null || item.GetComponent<MeleeWeapon>() != null;
return item.GetComponent<IdCard>() != null || IsWeapon(item);
}
static bool IsWeapon(Item item)
{
//a threshold of 10 excludes things like tools, all "proper weapons" seem to have a priority higher than that
return item.Components.Max(c => c.CombatPriority) > 10.0f || item.HasTag(Tags.Weapon);
}
if (foundItem == null) { return; }
bool isIdCard = foundItem.GetComponent<IdCard>() != null;
bool isWeapon = foundItem.GetComponent<RangedWeapon>() != null || foundItem.GetComponent<MeleeWeapon>() != null;
if (isIdCard)
{
string name = string.Empty;
@@ -325,7 +330,7 @@ namespace Barotrauma
JobPrefab clientJob = yoinker.CharacterInfo?.Job?.Prefab;
// security officers receive less karma penalty
if (clientJob != null && clientJob.Identifier == "securityofficer" && isWeapon)
if (clientJob != null && clientJob.Identifier == "securityofficer" && IsWeapon(foundItem))
{
karmaDecrease *= 0.5f;
}

View File

@@ -32,6 +32,13 @@ namespace Barotrauma.Networking
Port = serverSettings.Port,
DualStack = GameSettings.CurrentConfig.UseDualModeSockets
};
if (NetConfig.UseLenientHandshake)
{
// More lenient timeouts for local testing, so the server would start even without perfect conditions
netPeerConfiguration.ConnectionTimeout = 60.0f;
netPeerConfiguration.ResendHandshakeInterval = 5.0f;
netPeerConfiguration.MaximumHandshakeAttempts = 20;
}
netPeerConfiguration.DisableMessageType(
NetIncomingMessageType.DebugMessage

View File

@@ -582,6 +582,23 @@ namespace Barotrauma.Networking
teamSpecificState.RespawnItems.AddRange(AutoItemPlacer.RegenerateLoot(respawnShuttle, respawnContainer));
}
}
else if (character.InWater)
{
if (divingSuitPrefab != null)
{
var divingSuit = new Item(divingSuitPrefab, character.Position, respawnSub);
Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit));
character.Inventory.TryPutItem(divingSuit, user: null, allowedSlots: divingSuit.AllowedSlots);
teamSpecificState.RespawnItems.Add(divingSuit);
if (oxyPrefab != null && divingSuit.GetComponent<ItemContainer>() != null)
{
var oxyTank = new Item(oxyPrefab, character.Position, respawnSub);
Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank));
divingSuit.Combine(oxyTank, user: null);
teamSpecificState.RespawnItems.Add(oxyTank);
}
}
}
var characterData = campaign?.GetClientCharacterData(clients[i]);
// NOTE: This was where Reaper's tax got applied

View File

@@ -46,7 +46,7 @@ namespace Barotrauma.Networking
.Aggregate(NetFlags.None, (f1, f2) => f1 | f2);
private bool IsFlagRequired(Client c, NetFlags flag)
=> NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate);
=> NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate) || !c.InitialLobbyUpdateSent;
public NetFlags GetRequiredFlags(Client c)
=> LastUpdateIdForFlag.Keys

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.11.5.0</Version>
<Version>1.12.6.2</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -45,6 +45,11 @@
<Inventory slots="Any, Any, Any, Any" accessiblewhenalive="False" commonness="50">
<Item identifier="alienblood" />
</Inventory>
<StatusEffect type="OnDeconstructed" target="Character">
<SpawnItem identifiers="alienblood" spawnposition="ThisInventory" count="3" />
</StatusEffect>
<ai CombatStrength="100" Sight="1" Hearing="1" AggressionHurt="200" AggressionGreed="10" FleeHealthThreshold="10" AttackWhenProvoked="False" AvoidGunfire="True" DamageThreshold="0" AvoidTime="3" MinFleeTime="20" AggressiveBoarding="True" EnforceAggressiveBehaviorForMissions="True" TargetOuterWalls="True" RandomAttack="False" CanOpenDoors="False" KeepDoorsClosed="False" AvoidAbyss="True" StayInAbyss="False" PatrolFlooded="False" PatrolDry="False" StartAggression="0" MaxAggression="100" AggressionCumulation="0" WallTargetingMethod="Target">
<target Tag="decoy" State="Attack" Priority="500" ReactDistance="0" AttackDistance="0" Timer="0" IgnoreContained="False" IgnoreInside="False" IgnoreOutside="False" IgnoreIfNotInSameSub="True" IgnoreIncapacitated="False" Threshold="0" ThresholdMin="-1" ThresholdMax="-1" Offset="0,0" AttackPattern="Straight" PrioritizeSubCenter="False" SweepDistance="0" SweepStrength="10" SweepSpeed="1" CircleStartDistance="5000" CircleRotationSpeed="1" CircleStrikeDistanceMultiplier="5" CircleMaxRandomOffset="0" />
<target Tag="stronger" State="Avoid" Priority="200" ReactDistance="2000" AttackDistance="0" Timer="0" IgnoreContained="False" IgnoreInside="False" IgnoreOutside="False" IgnoreIfNotInSameSub="False" IgnoreIncapacitated="False" Threshold="0" ThresholdMin="-1" ThresholdMax="-1" Offset="0,0" AttackPattern="Straight" PrioritizeSubCenter="False" SweepDistance="0" SweepStrength="10" SweepSpeed="1" CircleStartDistance="5000" CircleRotationSpeed="1" CircleStrikeDistanceMultiplier="5" CircleMaxRandomOffset="0" />

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="Lighting stress (10000 lights)" modversion="1.0.0" corepackage="False" gameversion="1.11.5.0">
<Submarine file="%ModDir%/Lighting stress (10000 lights).sub" />
</contentpackage>

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,23 @@
<Items>
<Item
name="Oxygen Dispenser Test"
identifier="oxygendispensertest"
tags="oxygengenerator,refuelableitem,donttakeitemstorefill"
category="Machine"
scale="0.5"
isshootable="true" GrabWhenSelected="true">
<Sprite texture="%ModDir%/EthanolPowerGenerator.png" depth="0.55" sourcerect="0,336,112,128"/>
<Body width="112" height="128" density="25" />
<Holdable selectkey="Select" pickkey="Use" slots="RightHand+LeftHand" msg="ItemMsgDetach" MsgWhenDropped="ItemMsgPickupSelect" PickingTime="5.0" holdpos="0,-80" handle1="-30,14" handle2="30,14" attachable="true" aimable="true" AttachesToFloor="true"
AttachedByDefault="true" DisallowAttachingOverTags="container,planter,refuelableitem" DisallowAttachingOverSize="115,130">
</Holdable>
<ItemContainer hideitems="false" drawinventory="true" ItemsUseInventoryPlacement="true" capacity="1" maxstacksize="1" canbeselected="true" itempos="-25,-20" iteminterval="0,0" itemrotation="0" msg="ItemMsgOxygenRefill" containedspritedepth="0.1">
<GuiFrame relativesize="0.2,0.25" anchor="Center" minsize="140,170" maxsize="280,280" style="ItemUI" />
<SlotIcon slotindex="0" texture="Content/UI/StatusMonitorUI.png" sourcerect="64,448,64,64" origin="0.5,0.5" />
<Containable items="oxygensource" />
</ItemContainer>
</Item>
</Items>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Items>
<Item name="fliptestholdable" identifier="fliptestholdable" Category="Misc" Tags="smallitem" health="35" maxstacksize="1" scale="0.5" isshootable="true" requireaimtouse="true">
<sprite texture="Content/Map/Outposts/Art/FactionItems.png" sourcerect="263,193,38,39" depth="0.2" origin="0.5,0.5" />
<Body radius="28" density="15" />
<LightComponent LightColor="220,150,30,150" range="15" IsOn="true" castshadows="false" lightoffset="40,20" vulnerabletoemp="false" >
<IsActiveConditional HasStatusTag="smoking" />
<StatusEffect OffsetCopiesEntityTransform="true" offset="40,20" type="OnUse" target="This" statuseffecttags="smoking" duration="0.1" stackable="false">
<ParticleEmitter particle="blooddrop" particlespersecond="10" scalemin="3" scalemax="3" velocitymin="0" velocitymax="0" colormultiplier="255,255,255,180" lifetimemultiplier="2"/>
<ParticleEmitter particle="smoke" particlespersecond="3" scalemin="0.35" scalemax="0.5" velocitymin="0" velocitymax="10" colormultiplier="255,255,255,200" />
</StatusEffect>
</LightComponent>
<Holdable slots="Any,RightHand,LeftHand" aimable="false" aimpos="32,21" handle1="0,-22" holdangle="0" aimangle="-25" swingamount="0,0" swingspeed="0.5" swingwhenusing="true" msg="ItemMsgPickUpSelect">
<StatusEffect type="OnUse" target="This" Condition="-4.0" />
<StatusEffect type="OnUse" target="This">
<Conditional InWater="false" />
<Sound file="Content/Items/Medical/ITEM_cigarette.ogg" range="250" loop="true" selectionmode="Random" />
</StatusEffect>
<StatusEffect type="OnBroken" target="This">
<SpawnItem identifier="bananapeel" spawnposition="SameInventory"/>
<Remove />
</StatusEffect>
</Holdable>
</Item>
<Item name="fliptestlight" identifier="fliptestlighttower" width="176" height="352" texturescale="1.0,1.0" scale="0.5" category="Decorative" subcategory="mining" noninteractable="true">
<sprite texture="Content/Map/Outposts/Art/TunnelWalls.png" sourcerect="849,1697,176,352" depth="0.97" premultiplyalpha="false" origin="0.5,0.5" />
<LightComponent range="160.0" lightcolor="255,234,181,200" IsOn="true" castshadows="false" LightOffset="200,147" allowingameediting="false">
<sprite texture="Content/Map/Outposts/Art/TunnelWalls.png" sourcerect="671,1697,176,62" depth="0.1" origin="0.5,0.5" alpha="1.0" />
<StatusEffect OffsetCopiesEntityTransform="true" offset="200,147" type="OnActive" target="This" duration="0.1" stackable="false">
<ParticleEmitter particle="blooddrop" particlespersecond="10" scalemin="3" scalemax="3" velocitymin="0" velocitymax="0" colormultiplier="255,255,255,180" lifetimemultiplier="2"/>
<ParticleEmitter particle="smoke" particlespersecond="3" scalemin="0.35" scalemax="0.5" velocitymin="0" velocitymax="10" colormultiplier="255,255,255,200" />
</StatusEffect>
</LightComponent>
</Item>
</Items>

View File

@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="[DebugOnlyTest]RotationAndFlippingTests" modversion="1.0.2" corepackage="False" gameversion="1.7.6.0">
<contentpackage name="[DebugOnlyTest]RotationAndFlippingTests" modversion="1.0.3" corepackage="False" gameversion="1.11.5.0">
<Item file="%ModDir%/OxygenDispenserTest.xml" />
<Item file="%ModDir%/StatusEffectAndLightTest.xml" />
<Submarine file="%ModDir%/RotationAndFlippingTests.sub" />
</contentpackage>

View File

@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<RandomEvents>
<EventPrefabs>
<ScriptedEvent identifier="testpathfinding1" tags="testpathfinding_colony">
<SpawnAction NPCSetIdentifier="customnpcs1" NPCIdentifier="artiedolittle" TargetTag="npc1" SpawnPointTag="spawnpoint1" />
<SpawnAction NPCSetIdentifier="customnpcs1" NPCIdentifier="clownmessenger" TargetTag="npc2" SpawnPointTag="spawnpoint2" />
<SpawnAction NPCSetIdentifier="customnpcs1" NPCIdentifier="jacovsubra" TargetTag="npc3" SpawnPointTag="spawnpoint3" />
<SpawnAction NPCSetIdentifier="customnpcs1" NPCIdentifier="coalitionspy" TargetTag="npc4" SpawnPointTag="spawnpoint4" />
<SpawnAction NPCSetIdentifier="customnpcs1" NPCIdentifier="raptorowner" TargetTag="npc5" SpawnPointTag="spawnpoint5" />
<SpawnAction NPCSetIdentifier="customnpcs1" NPCIdentifier="hognose" TargetTag="npc6" SpawnPointTag="spawnpoint6" />
<SpawnAction NPCSetIdentifier="customnpcs1" NPCIdentifier="drugdealer" TargetTag="npc7" SpawnPointTag="spawnpoint7" />
<TagAction criteria="hullname:goalroom" tag="goal" />
<NPCFollowAction NPCTag="npc1" TargetTag="goal" />
<NPCFollowAction NPCTag="npc2" TargetTag="goal" />
<NPCFollowAction NPCTag="npc3" TargetTag="goal" />
<NPCFollowAction NPCTag="npc4" TargetTag="goal" />
<NPCFollowAction NPCTag="npc5" TargetTag="goal" forcewalk="true" />
<NPCFollowAction NPCTag="npc6" TargetTag="goal" forcewalk="true" />
<NPCFollowAction NPCTag="npc7" TargetTag="goal" forcewalk="true" />
<ConversationAction Text="Spawned 7 test NPCs. They should now navigate to the furthest module at the right side of the outpost. You may fast-forward by 60 seconds to skip to the end of the test." />
<WaitAction time="60" />
<CheckVisibilityAction EntityTag="npc1" TargetTag="goal" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 1 (Artie Dolittle) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc2" TargetTag="goal" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 2 (Clown Messenger) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc3" TargetTag="goal" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 3 (Jacov Subra) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc4" TargetTag="goal" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 4 (Coalition Operative) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc5" TargetTag="goal" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 5 (Severo Ruiz) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc6" TargetTag="goal" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 6 (Captain Hognose) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc7" TargetTag="goal" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 7 (Drug Dealer) did not make it to the target module in time." />
</Failure>
<Success>
<!-- ALL SUCCEEDED ******************************* -->
<ConversationAction Text="NPC test successful! All NPCs made it to the target module in time." />
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
<ConversationAction Text="Starting second test: making the NPCs navigate back to the left side of the outpost." />
<TagAction criteria="hullname:goalroom2" tag="goal2" />
<NPCFollowAction NPCTag="npc1" TargetTag="goal2" forcewalk="true" />
<WaitAction time="2" />
<NPCFollowAction NPCTag="npc2" TargetTag="goal2" forcewalk="true" />
<WaitAction time="2" />
<!-- follow another NPC instead of going directly for the goal! -->
<NPCFollowAction NPCTag="npc3" TargetTag="npc2" forcewalk="true" />
<WaitAction time="2" />
<NPCFollowAction NPCTag="npc4" TargetTag="goal2" />
<WaitAction time="2" />
<NPCFollowAction NPCTag="npc5" TargetTag="goal2" />
<WaitAction time="2" />
<NPCFollowAction NPCTag="npc6" TargetTag="goal2" />
<WaitAction time="2" />
<!-- follow another NPC instead of going directly for the goal! -->
<NPCFollowAction NPCTag="npc7" TargetTag="npc4" />
<WaitAction time="100" />
<CheckVisibilityAction EntityTag="npc1" TargetTag="goal2" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 1 (Artie Dolittle) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc2" TargetTag="goal2" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 2 (Clown Messenger) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc3" TargetTag="goal2" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 3 (Jacov Subra) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc4" TargetTag="goal2" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 4 (Coalition Operative) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc5" TargetTag="goal2" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 5 (Severo Ruiz) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc6" TargetTag="goal2" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 6 (Captain Hognose) did not make it to the target module in time." />
</Failure>
<Success>
<CheckVisibilityAction EntityTag="npc7" TargetTag="goal2" MaxDistance="500">
<Failure>
<ConversationAction Text="Test failed. NPC 7 (Drug Dealer) did not make it to the target module in time." />
</Failure>
<Success>
<!-- ALL SUCCEEDED ******************************* -->
<ConversationAction Text="NPC test successful! All NPCs made it to the target module in time." />
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</Success>
</CheckVisibilityAction>
</ScriptedEvent>
</EventPrefabs>
</RandomEvents>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="[DebugOnlyTest]TestPathFinding" modversion="1.0.0" corepackage="False" gameversion="1.12.4.0">
<Outpost file="%ModDir%/[DebugOnlyTest]TestPathFinding.sub" />
<RandomEvents file="%ModDir%/Events.xml" />
</contentpackage>

View File

@@ -1,38 +0,0 @@
BAROTRAUMA
http://www.barotraumagame.com
© 2017-2024 FakeFish Ltd. All rights reserved.
© 2019-2024 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved.
Privacy policy: http://privacypolicy.daedalic.com
See the wiki for more detailed info and instructions:
http://barotraumagame.com/wiki
------------------------------------------------------------------------
Port forwarding:
You may try to forward ports on your router using UPnP (Universal Plug and
Play) port forwarding by selecting "Attempt UPnP port forwarding" in the
"Host Server" menu.
However, UPnP isn't supported by all routers, so you may need to setup port
forwards manually. The exact steps for forwarding a port depend on your
router's model, but you may be able to find a port forwarding guide for
your particular router/application on portforward.com or by practicing
your google-fu skills.
These are the values that you should use when forwarding a port to your
Barotrauma server:
Game port (used to communicate with clients)
Service/Application: barotrauma
External Port: The port you have selected for your server (27015 by default)
Internal Port: The port you have selected for your server (27015 by default)
Protocol: UDP
Query port (used to communicate with Steam)
Service/Application: barotrauma
External Port: The port you have selected for your server (27016 by default)
Internal Port: The port you have selected for your server (27016 by default)
Protocol: UDP

View File

@@ -551,9 +551,14 @@ namespace Barotrauma
private static void UnlockKillAchievement(Character killer, Character target, Identifier identifier)
{
if (killer != null &&
target.Params.UnlockKillAchievementForWholeCrew &&
GameSession.GetSessionCrewCharacters(CharacterType.Player).Contains(killer))
bool alwaysUnlockForWholeCrew = false;
#if CLIENT
alwaysUnlockForWholeCrew = GameMain.GameSession?.Campaign is SinglePlayerCampaign;
#endif
if (killer != null &&
(alwaysUnlockForWholeCrew || target.Params.UnlockKillAchievementForWholeCrew) &&
GameSession.GetSessionCrewCharacters(CharacterType.Both).Contains(killer))
{
UnlockAchievement(identifier, unlockClients: true, characterConditions: c => c != null);
}

View File

@@ -49,6 +49,13 @@ namespace Barotrauma
}
}
/// <summary>
/// A multiplier for the sound range for the purposes of displaying the target on sonar.
/// E.g. a value of 10 would mean the sonar can detect the target from x10 further than monsters.
/// </summary>
public float SoundRangeOnSonarMultiplier { get; private set; } = 1.0f;
public float SightRange
{
get { return sightRange; }
@@ -206,6 +213,7 @@ namespace Barotrauma
MinSoundRange = element.GetAttributeFloat("minsoundrange", 0f);
MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange);
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange);
SoundRangeOnSonarMultiplier = element.GetAttributeFloat(nameof(SoundRangeOnSonarMultiplier), 1.0f);
FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime);
Static = element.GetAttributeBool("static", Static);
StaticSight = element.GetAttributeBool("staticsight", StaticSight);

View File

@@ -244,14 +244,39 @@ namespace Barotrauma
/// <summary>
/// The monster won't try to damage these submarines
/// </summary>
public HashSet<Submarine> UnattackableSubmarines
private readonly HashSet<Submarine> unattackableSubmarines = new HashSet<Submarine>();
public void SetUnattackableSubmarines(Submarine submarine, bool includeOwnSub = true, bool includeConnectedSubs = true, bool clearExisting = true)
{
get;
private set;
} = new HashSet<Submarine>();
if (clearExisting)
{
unattackableSubmarines.Clear();
}
if (submarine != null)
{
AddSubs(submarine);
}
if (includeOwnSub && Character.Submarine is Submarine ownSub && ownSub != submarine)
{
AddSubs(ownSub);
}
void AddSubs(Submarine sub)
{
unattackableSubmarines.Add(sub);
if (includeConnectedSubs)
{
foreach (Submarine connectedSub in sub.DockedTo)
{
unattackableSubmarines.Add(connectedSub);
}
}
}
}
public static bool IsTargetBeingChasedBy(Character target, Character character)
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && enemyAI.State is AIState.Attack or AIState.Aggressive;
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
@@ -539,26 +564,7 @@ namespace Barotrauma
//doesn't do anything usually, but events may sometimes change monsters' (or pets' that use enemy AI) teams
Character.UpdateTeam();
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
{
var currPath = PathSteering.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
{
// Don't allow to jump from too high.
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
ignorePlatforms = height < allowedJumpHeight;
}
}
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
{
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
HandleLaddersAndPlatforms(deltaTime);
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character))
@@ -986,6 +992,69 @@ namespace Barotrauma
}
}
//how often the character can try ragdolling to drop down
private const float MaxDroppingInterval = 5.0f;
//last time the character tried ragdolling to drop down
private double lastDroppingTime;
//how long the character can stay ragdolled to drop down
private const float MaxDroppingTime = 1.0f;
//timer for the duration of the ragdolling
private float droppingTimer;
private void HandleLaddersAndPlatforms(float deltaTime)
{
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
{
var currPath = PathSteering.CurrentPath;
if (currPath is { CurrentNode: WayPoint currentNode })
{
Vector2 colliderBottom = Character.AnimController.GetColliderBottom();
if (Character.Submarine != currentNode.Submarine)
{
colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, Character.Submarine);
}
if (currentNode.SimPosition.Y < colliderBottom.Y)
{
// Don't allow to jump from too high.
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
Vector2 diff = currentNode.WorldPosition - Character.WorldPosition;
float height = ConvertUnits.ToSimUnits(Math.Abs(diff.Y));
ignorePlatforms = height < allowedJumpHeight;
//trying to head down ladders, but can't climb -> periodically try ragdolling to get down
//(may be required by large monsters like mudraptors to fit through hatches)
if (ignorePlatforms && !Character.CanClimb && PathSteering.IsCurrentNodeLadder &&
ConvertUnits.ToSimUnits(Math.Abs(diff.X)) < Character.AnimController.Collider.GetMaxExtent())
{
if (lastDroppingTime < Timing.TotalTime - MaxDroppingInterval)
{
Character.IsRagdolled = true;
Character.SetInput(InputType.Ragdoll, hit: false, held: true);
droppingTimer += deltaTime;
if (droppingTimer > MaxDroppingTime)
{
lastDroppingTime = Timing.TotalTime;
}
}
else
{
droppingTimer = 0.0f;
}
}
}
}
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
{
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
}
#region Idle
private void UpdateIdle(float deltaTime, bool followLastTarget = true)
@@ -1229,6 +1298,8 @@ namespace Barotrauma
return;
}
if (Character.IsAttachedToController()) { return; }
attackWorldPos = SelectedAiTarget.WorldPosition;
attackSimPos = SelectedAiTarget.SimPosition;
@@ -1751,6 +1822,7 @@ namespace Barotrauma
{
SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority);
State = AIState.Attack;
AttackLimb = null;
return;
}
}
@@ -1761,12 +1833,20 @@ namespace Barotrauma
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
if ((!canAttack || distance > margin) && !IsTryingToSteerThroughGap)
{
bool useManualSteering = false;
// Steer towards the target if in the same room and swimming
// Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering.
if (Character.CurrentHull != null &&
Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
(Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
{
if (CanSeeTarget(targetCharacter))
{
useManualSteering = true;
}
}
if (useManualSteering)
{
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - myPos));
@@ -2311,18 +2391,49 @@ namespace Barotrauma
{
float prio = 1 + limb.attack.Priority;
if (Character.AnimController.SimplePhysicsEnabled) { return prio; }
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
float distanceFactor = 1;
float distance = Vector2.Distance(limb.WorldPosition, attackPos);
float maxDistance = Math.Max(limb.attack.Range * 3, 1000);
if (distance > maxDistance)
{
// Far enough to ignore the attack.
return 0;
}
// Not in range, but relatively close. Let's use the distance factor as a multiplier.
float distanceFactor;
if (limb.attack.Ranged)
{
float min = 100;
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
if (distance < min)
{
// Too close -> smoothly but steeply reduce the preference (and prefer other attacks, like melee instead)
float t = MathUtils.InverseLerp(0, min, distance);
distanceFactor = MathHelper.Lerp(0.01f, 1, t * t);
}
else
{
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance));
}
}
else
{
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
if (distance <= limb.attack.Range)
{
// In range.
if (!Character.InWater)
{
// On dry land vertical distance works a bit differently, as we can't necessarily reach the target above/below us.
float verticalDistance = Math.Abs(limb.WorldPosition.Y - attackPos.Y);
if (verticalDistance > limb.attack.DamageRange)
{
// Most likely can't reach.
return 0;
}
}
// Highly prefer attacks which we can use to hit immediately.
return prio * 10;
}
float min = limb.attack.Range;
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance));
}
return prio * distanceFactor;
}
@@ -2521,6 +2632,7 @@ namespace Barotrauma
{
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority);
State = AIState.Attack;
AttackLimb = null;
return true;
}
}
@@ -2555,14 +2667,10 @@ namespace Barotrauma
return true;
}
}
if (damageTarget != null)
{
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, user: Character);
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, user: Character);
}
}
if (damageTarget == null) { return true; }
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
@@ -2609,10 +2717,24 @@ namespace Barotrauma
}
return true;
}
private const float VisibilityCheckStep = 0.2f;
private double lastVisibilityCheckTime;
private bool canSeeTarget;
/// <summary>
/// This method uses <see cref="Character.CanSeeTarget"/> and caches the results.
/// </summary>
private bool CanSeeTarget(ISpatialEntity target)
{
if (Timing.TotalTime > lastVisibilityCheckTime + VisibilityCheckStep)
{
canSeeTarget = Character.CanSeeTarget(target);
lastVisibilityCheckTime = Timing.TotalTime;
}
return canSeeTarget;
}
private float aimTimer;
private float visibilityCheckTimer;
private bool canSeeTarget;
private float sinTime;
private bool Aim(float deltaTime, ISpatialEntity target, Item weapon)
{
@@ -2630,13 +2752,7 @@ namespace Barotrauma
{
Character.CursorPosition -= Character.Submarine.Position;
}
visibilityCheckTimer -= deltaTime;
if (visibilityCheckTimer <= 0.0f)
{
canSeeTarget = Character.CanSeeTarget(target);
visibilityCheckTimer = 0.2f;
}
if (!canSeeTarget)
if (!CanSeeTarget(target))
{
SetAimTimer();
return false;
@@ -2817,7 +2933,10 @@ namespace Barotrauma
}
}
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3);
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
if (Character.AnimController.OnGround || Character.InWater)
{
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, maxVelocity: 10.0f);
}
}
}
else
@@ -2961,7 +3080,7 @@ namespace Barotrauma
{
if (aiTarget.Entity.Submarine.Info.IsWreck ||
aiTarget.Entity.Submarine.Info.IsBeacon ||
UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
unattackableSubmarines.Contains(aiTarget.Entity.Submarine))
{
continue;
}
@@ -3509,13 +3628,16 @@ namespace Barotrauma
{
if (targetCharacter.Submarine != null)
{
// Target is inside -> reduce the priority
valueModifier *= 0.5f;
if (Character.Submarine != null)
if (Character.Submarine != null && !targetCharacter.Submarine.IsConnectedTo(Character.Submarine))
{
// Both inside different submarines -> can ignore safely
// Both inside different, unconnected submarines -> can ignore safely
continue;
}
else
{
// Target is inside a submarine that we are not -> reduce the priority
valueModifier *= 0.5f;
}
}
else if (Character.CurrentHull != null)
{
@@ -4402,6 +4524,7 @@ namespace Barotrauma
{
SelectTarget(doorAiTarget, CurrentTargetMemory.Priority);
State = AIState.Attack;
AttackLimb = null;
return false;
}
}

View File

@@ -1380,7 +1380,7 @@ namespace Barotrauma
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0;
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectionType) > 0;
// Inform other NPCs
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{

View File

@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Linq;
using FarseerPhysics;
using System.Diagnostics;
namespace Barotrauma
{
@@ -50,7 +51,7 @@ namespace Barotrauma
}
/// <summary>
/// Returns true if any node in the path is in stairs
/// Returns true if any node in the path is on stairs
/// </summary>
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
@@ -285,14 +286,17 @@ namespace Barotrauma
}
}
Vector2 diff = DiffToCurrentNode();
Vector2 diff = GetDiffAndAdvance();
if (diff == Vector2.Zero) { return Vector2.Zero; }
return Vector2.Normalize(diff) * weight;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight);
private Vector2 DiffToCurrentNode()
/// <summary>
/// Decides whether and when we should skip to the next node. Returns the difference to the current node (after skipping).
/// </summary>
private Vector2 GetDiffAndAdvance()
{
if (currentPath == null || currentPath.Unreachable)
{
@@ -320,26 +324,37 @@ namespace Barotrauma
Reset();
return Vector2.Zero;
}
Vector2 pos = host.WorldPosition;
Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
WayPoint currentNode = currentPath.CurrentNode;
WayPoint nextNode = currentPath.NextNode;
Vector2 diff = currentNode.WorldPosition - host.WorldPosition;
float horizontalDistance = Math.Abs(diff.X);
float verticalDistance = Math.Abs(diff.Y);
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
bool canClimb = character.CanClimb;
Ladder currentLadder = GetCurrentLadder();
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
Ladder ladders = currentLadder ?? nextLadder;
bool useLadders = canClimb && ladders != null;
var collider = character.AnimController.Collider;
Vector2 colliderSize = collider.GetSize();
Vector2 colliderSize = ConvertUnits.ToDisplayUnits(collider.GetSize());
float colliderHeight = colliderSize.Y;
if (character.AnimController.CurrentAnimationParams is FishGroundedParams fishGrounded)
{
// On monsters, the main collider might be rotated, so we need to take that into account here.
float standAngle = fishGrounded.ColliderStandAngleInRadians * character.AnimController.Dir;
Vector2 transformedColliderSize = PhysicsBody.RotateVector(colliderSize, standAngle);
colliderHeight = Math.Abs(transformedColliderSize.Y);
}
if (useLadders)
{
if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
if (character.IsClimbing && Math.Abs(diff.X) - colliderSize.X > Math.Abs(diff.Y))
{
// If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
useLadders = false;
}
else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
else if (!character.IsClimbing && nextNode != null && nextLadder == null)
{
Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
Vector2 diffToNextNode = nextNode.WorldPosition - host.WorldPosition;
if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
{
// If the next node is horizontally farther from us than vertically, we don't want to start climbing.
@@ -356,7 +371,7 @@ namespace Barotrauma
{
if (currentPath.IsAtEndNode && canClimb && ladders != null)
{
// Don't release the ladders when ending a path in ladders.
// Don't release the ladders when ending a path on ladders.
useLadders = true;
}
else
@@ -388,20 +403,18 @@ namespace Barotrauma
if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
{
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
NextNode(!doorsChecked);
return NextNode(!doorsChecked);
}
else
{
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
float colliderHeight = collider.Height / 2 + collider.Radius;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
float distanceMargin = colliderSize.X;
if (currentLadder != null && nextLadder != null)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
if (Math.Abs(heightDiff) < colliderHeight * 1.25f)
if (verticalDistance < colliderHeight / 2 * 1.25f)
{
if (nextLadder != null && !nextLadderSameAsCurrent)
{
@@ -410,7 +423,7 @@ namespace Barotrauma
{
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
{
NextNode(!doorsChecked);
return NextNode(!doorsChecked);
}
}
}
@@ -432,9 +445,9 @@ namespace Barotrauma
}
if (isAboveFloor)
{
if (Math.Abs(diff.Y) < distanceMargin)
if (verticalDistance < distanceMargin)
{
NextNode(!doorsChecked);
return NextNode(!doorsChecked);
}
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
{
@@ -443,14 +456,21 @@ namespace Barotrauma
}
}
}
else if (currentLadder != null && currentPath.NextNode != null)
else if (currentLadder != null && nextNode != null)
{
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
if (Math.Sign(currentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(nextNode.WorldPosition.Y - character.WorldPosition.Y))
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
NextNode(!doorsChecked);
return NextNode(!doorsChecked);
}
//heading towards a ladder waypoint below the character, but the next waypoint is above it on the same ladder
// -> allow skipping to that waypoint.
// Otherwise the character may get stuck trying to move to a waypoint near the floor at the bottom of the ladder, failing to get close enough because they can't move any lower.
else if (nextLadderSameAsCurrent && diff.Y < 0 && nextNode.WorldPosition.Y > currentNode.WorldPosition.Y)
{
return NextNode(!doorsChecked);
}
}
}
@@ -458,21 +478,20 @@ namespace Barotrauma
else if (character.AnimController.InWater)
{
// Swimming
var door = currentPath.CurrentNode.ConnectedDoor;
var door = currentNode.ConnectedDoor;
if (door == null || door.CanBeTraversed)
{
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
float distanceMultiplier = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * distanceMultiplier, 0.5f);
float modifiedVerticalDist = verticalDistance;
if (character.CurrentHull != currentNode.CurrentHull)
{
verticalDistance *= 2;
modifiedVerticalDist *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
float distance = horizontalDistance + modifiedVerticalDist;
if (distance < targetDistance)
{
NextNode(!doorsChecked);
return NextNode(!doorsChecked);
}
}
}
@@ -480,6 +499,10 @@ namespace Barotrauma
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
if (character.Submarine != currentNode.Submarine)
{
colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, character.Submarine);
}
Vector2 velocity = collider.LinearVelocity;
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
@@ -487,60 +510,113 @@ namespace Barotrauma
float minHeight = 1.6125001f;
float minWidth = 0.3225f;
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
float colliderHeight = collider.Height / 2 + collider.Radius;
if (currentPath.CurrentNode.Stairs == null)
float characterHeight = Math.Max(ConvertUnits.ToSimUnits(colliderHeight) + character.AnimController.ColliderHeightFromFloor, minHeight);
bool isTargetTooHigh = currentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
bool isTargetTooLow = currentNode.SimPosition.Y < colliderBottom.Y;
var door = currentNode.ConnectedDoor;
float targetDistanceMultiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
if (currentNode.Stairs == null)
{
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
// Only attempt dropping if the node is below the collider bottom.
// Using the next node position here, because the current node might be on the top of the ladder, which can be at the same level with the character or even above it.
bool isBelowEnough = (nextNode ?? currentNode).WorldPosition.Y < character.WorldPosition.Y - colliderHeight / 2;
bool drop = false;
if (isBelowEnough)
{
// Original comment:
//the waypoint is between the top and bottom of the collider, no need to move vertically.
// Note that the waypoint can be below collider too! This might be incorrect.
if (!canClimb)
{
// Can't climb -> check if we should drop.
Door nextDoor = door ?? nextNode?.ConnectedDoor;
if (nextDoor is Door { IsHorizontal: true, CanBeTraversed: true } openHatch)
{
bool isHatchBelowCharacter = openHatch.LinkedGap.WorldPosition.Y < character.WorldPosition.Y;
if (isHatchBelowCharacter)
{
// Trying to go through an open hatch below us -> drop.
drop = true;
}
}
else if (currentLadder != null && !isTargetTooLow && nextDoor == null)
{
// On ladders -> drop.
drop = true;
}
}
}
if (drop)
{
return NextNode(!doorsChecked);
}
else if (verticalDistance < colliderHeight / 2)
{
// The waypoint is between the top and bottom of the collider, and we don't intend to drop -> no need to move vertically.
diff.Y = 0.0f;
}
}
else
{
// In stairs
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
// On stairs
bool isNextNodeInSameStairs = nextNode?.Stairs == currentNode.Stairs;
if (!isNextNodeInSameStairs)
{
margin = 1;
if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
targetDistanceMultiplier = 1;
if (currentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
{
isTargetTooLow = true;
}
Structure nextStairs = nextNode?.Stairs;
if (character.AnimController.Stairs != null && nextStairs != null)
{
//currently on stairs, and the next node is not in the same stairs
// -> we must get off the current stairs first before we can skip to the next node, otherwise the character
// would attempt to get "through the stairs" to the next ones
if (character.AnimController.Stairs.StairDirection == Direction.Right)
{
//the direction in which the bot should keep moving depends on the direction of the stairs and whether we're going up or down
diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? Vector2.UnitX : -Vector2.UnitX;
}
else
{
diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? -Vector2.UnitX : Vector2.UnitX;
}
}
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
// Walking horizontally, check whether we are close enough to the current node.
float targetDistance = Math.Max(colliderSize.X / 2 * targetDistanceMultiplier, ConvertUnits.ToDisplayUnits(minWidth / 2));
Debug.Assert(targetDistance < 500, "Target distance too large (a character is trying to skip on their path to a waypoint far away), something is probably off here.");
if (!isTargetTooHigh && !isTargetTooLow && horizontalDistance < targetDistance)
{
if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
bool isBlockedByDoor = door is { CanBeTraversed: false };
// If both the current ladder and the next ladder are not null, we are in the middle of ladders and should let the code above handle advancing the nodes.
// However, if either one is null, and we get here, we are probably walking to or from ladders.
bool notOnLadders = currentLadder == null || nextLadder == null;
if (!isBlockedByDoor && notOnLadders)
{
NextNode(!doorsChecked);
return NextNode(!doorsChecked);
}
}
}
if (currentPath.CurrentNode == null)
return ReturnDiff();
Vector2 NextNode(bool checkDoors)
{
return Vector2.Zero;
if (checkDoors)
{
CheckDoorsInPath();
}
currentPath.SkipToNextNode();
return ReturnDiff();
}
return ConvertUnits.ToSimUnits(diff);
}
private void NextNode(bool checkDoors)
{
if (checkDoors)
Vector2 ReturnDiff()
{
CheckDoorsInPath();
if (currentPath.CurrentNode == null)
{
return Vector2.Zero;
}
return ConvertUnits.ToSimUnits(diff);
}
currentPath.SkipToNextNode();
}
public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
@@ -600,8 +676,6 @@ namespace Barotrauma
}
}
private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
private float GetColliderLength()
{
Vector2 colliderSize = character.AnimController.Collider.GetSize();
@@ -676,7 +750,7 @@ namespace Barotrauma
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
float size = character.AnimController.InWater ? colliderLength : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()).X;
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size;
}
else
@@ -794,12 +868,17 @@ namespace Barotrauma
if (character == null) { return 0.0f; }
float? penalty = GetSingleNodePenalty(nextNode);
if (penalty == null) { return null; }
Vector2 nextNodePosition = nextNode.Position;
if (nextNode.Waypoint.Submarine != node.Waypoint.Submarine)
{
nextNodePosition = Submarine.GetRelativeSimPosition(nextNodePosition, node.Waypoint.Submarine, nextNode.Waypoint.Submarine);
}
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
if (!character.CanClimb && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNodeAboveWaterLevel)) //upper node not underwater
(nextNodePosition.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNodeAboveWaterLevel)) //upper node not underwater
{
return null;
}
@@ -830,7 +909,7 @@ namespace Barotrauma
}
}
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
float yDist = Math.Abs(node.Position.Y - nextNodePosition.Y);
if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
{
penalty += yDist * 10.0f;
@@ -898,18 +977,14 @@ namespace Barotrauma
//steer away from edges of the hull
bool wander = false;
bool inWater = character.AnimController.InWater;
Hull currentHull = character.CurrentHull;
// TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere.
// if (!inWater)
// {
// Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom());
// if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull)
// {
// // Use the hull found at the collider bottom, if found.
// // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is.
// currentHull = lowestHull;
// }
// }
//use the hull the legs are in (if one is found), so the character won't walk against the wall when their torso is in a different hull where there'd be room to walk further
//(e.g. if the character is in a shallow pool-type room, like in ResearchModule_01_Colony)
Hull currentHull =
character.AnimController.GetLimb(LimbType.RightLeg)?.Hull ??
character.AnimController.GetLimb(LimbType.LeftLeg)?.Hull ??
character.CurrentHull;
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;

View File

@@ -103,9 +103,19 @@ namespace Barotrauma
}
}
// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
public bool ForceWalk { get; set; }
/// <summary>
/// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
/// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
/// </summary>
public bool ForceWalkTemporarily { get; set; }
/// <summary>
/// Forces the character to walk when executing this objective, even if the priority is above <see cref="AIObjectiveManager.RunPriority"/>.
/// Unlike <see cref="ForceWalkTemporarily"/>, this value is not automatically reset.
/// </summary>
public bool ForceWalkPermanently { get; set; }
public bool ForceWalk => ForceWalkTemporarily || ForceWalkPermanently;
public bool IgnoreAtOutpost { get; set; }
@@ -313,7 +323,7 @@ namespace Barotrauma
/// </summary>
public float CalculatePriority()
{
ForceWalk = false;
ForceWalkTemporarily = false;
Priority = GetPriority();
ForceHighestPriority = false;
return Priority;

View File

@@ -40,7 +40,7 @@ namespace Barotrauma
if (subObjectives.All(so => so.SubObjectives.None()))
{
// If none of the subobjectives have subobjectives, no valid container was found. Don't allow running.
ForceWalk = true;
ForceWalkTemporarily = true;
}
return prio;
}

View File

@@ -258,13 +258,14 @@ namespace Barotrauma
protected override bool CheckObjectiveState()
{
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
// In a friendly outpost, and the target is still in the outpost
if (character.Submarine is { Info.IsOutpost: true } && character.IsOnFriendlyTeam(character.Submarine.TeamID) &&
character.Submarine == Enemy.Submarine)
{
// Target still in the outpost
// Outpost guards shouldn't lose the target in friendly outposts,
// However, if we are not a guard, let's ensure that we allow the cooldown.
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity)
{
// Outpost guards shouldn't lose the target in friendly outposts,
// However, if we are not a guard, let's ensure that we allow the cooldown.
allowCooldown = true;
}
}
@@ -286,7 +287,8 @@ namespace Barotrauma
{
allowCooldown = true;
// Target not in the outpost anymore.
if (character.CanSeeTarget(Enemy))
if (character.Submarine.IsConnectedTo(Enemy.Submarine) &&
character.CanSeeTarget(Enemy))
{
allowCooldown = false;
coolDownTimer = DefaultCoolDown;
@@ -389,7 +391,7 @@ namespace Barotrauma
HumanAIController.AutoFaceMovement = false;
if (!gotoObjective.ShouldRun(true))
{
ForceWalk = true;
ForceWalkTemporarily = true;
}
}
}
@@ -468,7 +470,7 @@ namespace Barotrauma
isMoving = true;
if (!IsEnemyClose(MeleeDistance))
{
ForceWalk = true;
ForceWalkTemporarily = true;
}
HumanAIController.FaceTarget(Enemy);
HumanAIController.AutoFaceMovement = false;
@@ -1234,7 +1236,7 @@ namespace Barotrauma
}
if (isAimBlocked)
{
ForceWalk = true;
ForceWalkTemporarily = true;
}
if (!followTargetObjective.IsCloseEnough)
{

View File

@@ -95,7 +95,11 @@ namespace Barotrauma
if (potentialDeconstructor?.InputContainer == null) { continue; }
if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; }
if (!potentialDeconstructor.Item.HasAccess(character)) { continue; }
if (Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem))) { continue; }
if (Item.Prefab.DeconstructItems.Any() &&
Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem)))
{
continue;
}
float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f);
if (distFactor > bestDistFactor)
{

View File

@@ -64,7 +64,11 @@ namespace Barotrauma
if (target == null || target.Removed) { return false; }
//bots can't handle deconstructing items that require another item to deconstruct, let's not try to do that
//in the vanilla game, this means unidentified genetic materials, which we don't want to "deconstruct" anyway
if (target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0)) { return false; }
if (target.Prefab.DeconstructItems.Any() &&
target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0))
{
return false;
}
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true))

View File

@@ -148,7 +148,7 @@ namespace Barotrauma
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f);
}
// Prevents running into the flames.
objectiveManager.CurrentObjective.ForceWalk = true;
objectiveManager.CurrentObjective.ForceWalkTemporarily = true;
}
if (moveCloser)
{

View File

@@ -11,6 +11,8 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier();
public override bool ForceRun => true;
protected override bool AllowInAnySub => true;
// Periodically clear the ignore list so that fires abandoned when fumbling with finding an extinguisher, navigating etc get reconsidered
protected override float IgnoreListClearInterval => 30;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }

View File

@@ -49,6 +49,13 @@ namespace Barotrauma
public const float DefaultReach = 100;
public const float MaxReach = 150;
/// <summary>
/// How long it takes for the objective to be abandoned if no suitable item is found.
/// Intended to be an optimization: if the bots are constantly trying to find some item (like a diving suit),
/// it can easily lead to performance issues when e.g. AIObjectiveFindDivingGear constantly starts up new GetItem objectives.
/// </summary>
private float abandonDelayIfItemNotFound = 5.0f;
/// <summary>
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
/// If so, the objective won't attempt to create another objective if the path requires diving gear
@@ -213,7 +220,7 @@ namespace Barotrauma
{
if (isDoneSeeking)
{
HandlePotentialItems();
HandlePotentialItems(deltaTime);
}
if (objectiveManager.CurrentOrder is not AIObjectiveGoTo)
{
@@ -389,6 +396,8 @@ namespace Barotrauma
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character),
SpeakIfFails = false,
ForceWalkTemporarily = this.ForceWalkTemporarily,
ForceWalkPermanently = this.ForceWalkPermanently,
endNodeFilter = CreateEndNodeFilter(moveToTarget)
};
},
@@ -598,7 +607,7 @@ namespace Barotrauma
}
}
private void HandlePotentialItems()
private void HandlePotentialItems(float deltaTime)
{
Debug.Assert(isDoneSeeking);
if (itemCandidates.Any())
@@ -652,10 +661,14 @@ namespace Barotrauma
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
abandonDelayIfItemNotFound -= deltaTime;
if (abandonDelayIfItemNotFound <= 0.0f)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
}
@@ -718,13 +731,15 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
bool matchesIdentifiersOrTags = item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
if (!matchesIdentifiersOrTags) { return false; }
if (!item.HasAccess(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; }
return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
return true;
}
public override void Reset()

View File

@@ -958,6 +958,7 @@ namespace Barotrauma
public bool ShouldRun(bool run)
{
if (ForceWalk) { return false; }
if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam)
{
// NPCs with a wait order don't run.

View File

@@ -267,7 +267,10 @@ namespace Barotrauma
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
return true;
//don't stop at ladders when idling
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
}, endNodeFilter: node =>
node.Waypoint.Stairs == null && node.Waypoint.CurrentHull == currentTarget && node.Waypoint.Ladders == null &&
(!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room

View File

@@ -78,9 +78,10 @@ namespace Barotrauma
if (item.GetRootInventoryOwner() is Character targetCharacter &&
AIObjectiveFightIntruders.IsValidTarget(targetCharacter, character, targetCharactersInOtherSubs: false))
{
float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, aiTarget.SoundRange, distanceMultiplierPerClosedDoor: 2);
if (dist * HumanAIController.Hearing > aiTarget.SoundRange) { continue; }
float range = aiTarget.SoundRange * HumanAIController.Hearing;
float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, range, distanceMultiplierPerClosedDoor: 2);
if (dist > range) { continue; }
character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective))
{

View File

@@ -112,7 +112,7 @@ namespace Barotrauma
float prio = objectiveManager.GetOrderPriority(this);
if (subObjectives.All(so => so.SubObjectives.None() || so.Priority <= 0))
{
ForceWalk = true;
ForceWalkTemporarily = true;
}
return prio;
}

View File

@@ -257,6 +257,7 @@ namespace Barotrauma
{
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
TargetName = target.Item.Name,
ForceWalkPermanently = ForceWalk,
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
},
onAbandon: () => Abandon = true,

View File

@@ -29,7 +29,7 @@ namespace Barotrauma
{
if (pump?.Item == null || pump.Item.Removed) { return false; }
if (pump.Item.IgnoreByAI(character)) { return false; }
if (!pump.Item.IsInteractable(character)) { return false; }
if (!pump.Item.IsInteractable(character) || !pump.CanBeSelected) { return false; }
if (pump.IsAutoControlled) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull == null) { return false; }

View File

@@ -136,7 +136,7 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds)
{
ignoredAsMinorWounds = false;
if (target == null || target.IsDead || target.Removed) { return false; }
if (target == null || target.IsDead || target.Removed || target.InvisibleTimer > 0.0f) { return false; }
if (target.IsInstigator) { return false; }
if (target.IsPet) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }

View File

@@ -42,7 +42,9 @@ namespace Barotrauma
{
enemyAi.PetBehavior?.Update(deltaTime);
}
if (IsDead || IsUnconscious || Stun > 0.0f || IsIncapacitated)
if (IsDead || IsUnconscious || IsIncapacitated ||
//only check "real" stuns here, ignoring ragdolling, so the AI can run and decide whether to ragdoll or unragdoll
CharacterHealth.Stun > 0.0f)
{
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,

View File

@@ -685,7 +685,7 @@ namespace Barotrauma
{
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
if (Collider.BodyType == BodyType.Dynamic)
if (Collider.BodyType == BodyType.Dynamic && onGround)
{
Collider.LinearVelocity = new Vector2(
movement.X,

View File

@@ -30,6 +30,7 @@ namespace Barotrauma
{
public Fixture F1, F2;
public Vector2 LocalNormal;
public Vector2 WorldNormal;
public Vector2 Velocity;
public Vector2 ImpactPos;
@@ -39,7 +40,7 @@ namespace Barotrauma
F2 = f2;
Velocity = velocity;
LocalNormal = contact.Manifold.LocalNormal;
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
contact.GetWorldManifold(out WorldNormal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
ImpactPos = points[0];
}
}
@@ -826,7 +827,7 @@ namespace Barotrauma
return true;
}
private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity)
private void ApplyImpact(Fixture f1, Fixture f2, Vector2 worldNormal, Vector2 impactPos, Vector2 velocity)
{
if (character.DisableImpactDamageTimer > 0.0f) { return; }
@@ -838,7 +839,7 @@ namespace Barotrauma
return;
}
Vector2 normal = localNormal;
Vector2 normal = worldNormal;
float impact = Vector2.Dot(velocity, -normal);
if (f1.Body == Collider.FarseerBody || !Collider.Enabled)
{
@@ -1069,9 +1070,12 @@ namespace Barotrauma
}
Hull newHull = Hull.FindHull(findPos, currentHull);
if (setInWater && newHull == null)
if (setInWater)
{
inWater = true;
if (newHull == null || findPos.Y < newHull.WorldSurface)
{
inWater = true;
}
}
if (newHull == currentHull) { return; }
@@ -1114,7 +1118,10 @@ namespace Barotrauma
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; }
if (Limbs.Any(l => !l.IsSevered && Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null))
{
return;
}
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
}
@@ -1246,6 +1253,9 @@ namespace Barotrauma
private float BodyInRestDelay = 1.0f;
/// <summary>
/// Controls the sleeping state of this character
/// </summary>
public bool BodyInRest
{
get { return bodyInRestTimer > BodyInRestDelay; }
@@ -1269,7 +1279,7 @@ namespace Barotrauma
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
ApplyImpact(impact.F1, impact.F2, impact.WorldNormal, impact.ImpactPos, impact.Velocity);
}
CheckValidity();
@@ -1312,9 +1322,18 @@ namespace Barotrauma
}
float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
Collider.LinearVelocity = new Vector2(
NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12),
NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12));
if (GameMain.NetworkMember != null)
{
Collider.LinearVelocity = new Vector2(
NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12),
NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12));
}
else
{
Collider.LinearVelocity = new Vector2(
MathHelper.Clamp(Collider.LinearVelocity.X, -MaxVel, MaxVel),
MathHelper.Clamp(Collider.LinearVelocity.Y, -MaxVel, MaxVel));
}
if (forceStanding)
{
@@ -1368,9 +1387,19 @@ namespace Barotrauma
UpdateHullFlowForces(deltaTime);
if (currentHull == null ||
bool applyWaterForces =
currentHull == null ||
currentHull.WaterVolume > currentHull.Volume * 0.95f ||
ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y)
ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y;
#if CLIENT
if (Screen.Selected is CharacterEditor.CharacterEditorScreen &&
this is AnimController animController)
{
applyWaterForces = animController.CurrentAnimationParams is SwimParams;
}
#endif
if (applyWaterForces)
{
Collider.ApplyWaterForces();
}
@@ -1460,10 +1489,10 @@ namespace Barotrauma
else
{
// Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck.
if (Collider.LinearVelocity == Vector2.Zero && !character.IsRemotePlayer)
if (Collider.LinearVelocity == Vector2.Zero && GameMain.NetworkMember is not { IsClient: true })
{
character.IsRagdolled = true;
if (character.IsBot)
if (!character.IsPlayer)
{
// Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots.
character.SetInput(InputType.Ragdoll, hit: false, held: true);
@@ -1823,7 +1852,13 @@ namespace Barotrauma
{
floorFixture = standOnFloorFixture;
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f)
//allow the floor to be just a bit below the bottom of the collider for the character to be "on ground"
//there is some inaccuracy in the physics simulation (and floats), the collider isn't usually precisely ColliderHeightFromFloor above the floor
const float Tolerance = 0.1f;
float standHeight = Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor;
if (rayStart.Y - standOnFloorY <= standHeight + Tolerance)
{
onGround = true;
if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs)

View File

@@ -187,6 +187,11 @@ namespace Barotrauma
set => Params.Health.DoesBleed = value;
}
/// <summary>
/// Can this character be contained inside a controller?
/// </summary>
public bool IsContainable { get; set; }
public readonly Dictionary<Identifier, SerializableProperty> Properties;
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
@@ -683,6 +688,11 @@ namespace Barotrauma
get { return AnimController.Mass; }
}
/// <summary>
/// The position the character was at when we previously set the transforms of the items in the character's inventory.
/// </summary>
private Vector2 lastInventoryItemSetTransformPosition;
public CharacterInventory Inventory { get; private set; }
/// <summary>
@@ -788,7 +798,24 @@ namespace Barotrauma
set
{
if (value == selectedCharacter) { return; }
if (selectedCharacter != null) { selectedCharacter.selectedBy = null; }
//deselect the currently selected character
if (selectedCharacter != null)
{
selectedCharacter.selectedBy = null;
//check if some other character has selected the currently selected character too,
//and set selectedBy to that other character (otherwise the currently selected character would be unaware they're still being dragged by someone)
foreach (var otherCharacter in CharacterList)
{
if (otherCharacter != this && otherCharacter.selectedCharacter == selectedCharacter)
{
selectedCharacter.selectedBy = otherCharacter;
break;
}
}
}
CharacterHUD.RecreateHudTextsIfControlling(this);
selectedCharacter = value;
if (selectedCharacter != null) { selectedCharacter.selectedBy = this; }
#if CLIENT
@@ -1642,8 +1669,10 @@ namespace Barotrauma
AnimController.FindHull(setInWater: true);
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f);
CharacterList.Add(this);
Enabled = GameMain.NetworkMember == null;
if (info != null)
@@ -2268,6 +2297,12 @@ namespace Barotrauma
}
}
// Try to detach from the controller if we are currently attached to something that is dangerous for our character
if (aiControlled && Stun <= 0f && !IsKnockedDownOrRagdolled && !LockHands && ShouldAvoidStayingAttachedToController())
{
SelectedItem = null;
}
if (GameMain.NetworkMember != null)
{
if (GameMain.NetworkMember.IsServer)
@@ -2316,7 +2351,7 @@ namespace Barotrauma
{
attackCoolDown -= deltaTime;
}
else if (IsKeyDown(InputType.Attack))
else if (IsKeyDown(InputType.Attack) && !IsAttachedToController())
{
//normally the attack target, where to aim the attack and such is handled by EnemyAIController,
//but in the case of player-controlled monsters, we handle it here
@@ -2843,14 +2878,14 @@ namespace Barotrauma
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
#endif
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
Controller controller = item.GetComponent<Controller>();
if (controller != null && IsAnySelectedItem(item) && controller.IsAttachedUser(this))
{
return true;
}
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
if (item.ParentInventory != null)
{
return CanAccessInventory(item.ParentInventory);
@@ -2972,7 +3007,9 @@ namespace Barotrauma
}
}
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
//note that the distance to item should be set to 0 above if the character is within the item's bounding box
bool closeEnoughToIgnoreVisibilityCheck = distanceToItem <= 0.1f;
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger && !closeEnoughToIgnoreVisibilityCheck)
{
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
bool itemCenterVisible = CheckBody(body, item);
@@ -3001,7 +3038,6 @@ namespace Barotrauma
{
return itemCenterVisible;
}
}
return true;
@@ -3091,7 +3127,11 @@ namespace Barotrauma
if (!CanInteract)
{
SelectedItem = SelectedSecondaryItem = null;
if (!IsAttachedToController())
{
SelectedItem = null;
}
SelectedSecondaryItem = null;
focusedItem = null;
if (!AllowInput)
{
@@ -3110,8 +3150,16 @@ namespace Barotrauma
{
if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)
{
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; }
//don't allow focusing on anyone when the health window is open (avoids accidentally selecting someone when closing the window)
if (CharacterHealth.OpenHealthWindow != null)
{
FocusedCharacter = null;
}
else
{
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; }
}
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
{
@@ -3435,7 +3483,7 @@ namespace Barotrauma
obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f);
if (Inventory != null)
if (Inventory != null && Vector2.DistanceSquared(lastInventoryItemSetTransformPosition, Position) > 0.1f)
{
//do not check for duplicates: this is code is called very frequently, and duplicates don't matter here,
//so it's better just to avoid the relatively expensive duplicate check
@@ -3444,6 +3492,7 @@ namespace Barotrauma
if (item.body == null || item.body.Enabled) { continue; }
item.SetTransform(SimPosition, 0.0f, forceSubmarine: Submarine);
}
lastInventoryItemSetTransformPosition = Position;
}
HideFace = false;
@@ -3570,7 +3619,7 @@ namespace Barotrauma
{
wasRagdolled = IsRagdolled;
IsRagdolled = IsKeyDown(InputType.Ragdoll);
if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true })
if (IsRagdolled && !IsPlayer && GameMain.NetworkMember is not { IsClient: true })
{
ClearInput(InputType.Ragdoll);
}
@@ -3622,7 +3671,19 @@ namespace Barotrauma
AnimController.IgnorePlatforms = true;
}
AnimController.ResetPullJoints();
SelectedItem = SelectedSecondaryItem = null;
// Prevent us from detaching from the controller if we are attached to it OR detach if we
// manually ragdoll, in this case it should be similar to us deselecting the controller
if (!IsAttachedToController() ||
(IsKeyDown(InputType.Ragdoll)
// Let only the server do this check since the Ragdoll input for other clients is set to be held
// for stunned characters even if a character isn't manually ragdolling
&& (GameMain.NetworkMember == null || GameMain.NetworkMember is { IsServer: true } )))
{
SelectedItem = null;
}
SelectedSecondaryItem = null;
SelectedCharacter = null;
return;
}
@@ -3651,6 +3712,13 @@ namespace Barotrauma
bool MustDeselect(Item item)
{
if (item == null) { return false; }
// Prevent creatures from deselecting the controller if they are attached to it
if (IsAIControlled && !CanInteract && IsAttachedToController())
{
return false;
}
if (!CanInteractWith(item)) { return true; }
bool hasSelectableComponent = false;
foreach (var component in item.Components)
@@ -4376,6 +4444,41 @@ namespace Barotrauma
}
}
public void ForceSay(LocalizedString messageToSay, bool sayInRadio, bool removeQuotes = false, float delay = 0.0f)
{
if (messageToSay.IsNullOrEmpty() || SpeechImpediment >= 100.0f || IsDead)
{
return;
}
if (removeQuotes)
{
messageToSay = new TrimLString(messageToSay,
TrimLString.Mode.Both, ['"', '”', '“', ' ']);
}
ChatMessageType messageType = ChatMessageType.Default;
bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio);
if (canUseRadio && sayInRadio)
{
messageType = ChatMessageType.Radio;
}
CoroutineManager.Invoke(() =>
{
#if SERVER
GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, this);
#elif CLIENT
// no need to create the message when playing as a client, the server will send it to us
if (GameMain.Client == null)
{
AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType);
SendSinglePlayerMessage(message, canUseRadio, radio);
}
#endif
}, delay);
}
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
@@ -4760,6 +4863,10 @@ namespace Barotrauma
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
if (Screen.Selected != GameMain.GameScreen) { return; }
//don't allow stunning for less than one frame
//fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun,
//because even a one-frame stun briefly disables the animations and makes the character stop
if (newStun < Timing.Step && Stun <= 0.0f) { return; }
if (GodMode)
{
CharacterHealth.Stun = 0;
@@ -4787,7 +4894,12 @@ namespace Barotrauma
CharacterHealth.Stun = newStun;
if (newStun > 0.0f)
{
SelectedItem = SelectedSecondaryItem = null;
if (!IsAttachedToController())
{
SelectedItem = null;
}
SelectedSecondaryItem = null;
if (SelectedCharacter != null) { DeselectCharacter(); }
}
HealthUpdateInterval = 0.0f;
@@ -4976,6 +5088,37 @@ namespace Barotrauma
}
}
public bool IsAttachedToController()
{
if (SelectedItem == null) { return false; }
var controller = SelectedItem.GetComponent<Controller>();
if (controller == null) { return false; }
return controller.IsAttachedUser(this);
}
public bool ShouldAvoidStayingAttachedToController()
{
if (!IsAttachedToController()) { return false; }
var deconstructor = SelectedItem.GetComponent<Deconstructor>();
if (deconstructor != null)
{
return true;
}
// Character is being carried by an enemy!
if (IsHuman &&
SelectedItem.GetRootInventoryOwner() is Character carryingCharacter &&
TeamID != carryingCharacter.TeamID)
{
return true;
}
return false;
}
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
{
if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; }
@@ -5113,7 +5256,7 @@ namespace Barotrauma
AnimController.movement = Vector2.Zero;
AnimController.TargetMovement = Vector2.Zero;
if (!LockHands)
if (!LockHands && causeOfDeath != CauseOfDeathType.Disconnected)
{
foreach (Item heldItem in HeldItems.ToList())
{

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;

View File

@@ -629,7 +629,7 @@ namespace Barotrauma
public static readonly Identifier StunType = "stun".ToIdentifier();
public static readonly Identifier EMPType = "emp".ToIdentifier();
public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier();
public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier();
public static readonly Identifier AlienInfectionType = "alieninfection".ToIdentifier();
public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier();
public static readonly Identifier DisguisedAsHuskType = "disguiseashusk".ToIdentifier();

View File

@@ -827,9 +827,21 @@ namespace Barotrauma
}
}
float modifiedStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType));
if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
{
//don't allow stunning for less than one frame
//fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun,
//because even a one-frame stun briefly disables the animations and makes the character stop
if (modifiedStrength < Timing.Step && Stun <= 0.0f)
{
return;
}
}
if (existingAffliction != null)
{
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType));
float newStrength = modifiedStrength;
if (allowStacking)
{
// Add the existing strength
@@ -851,7 +863,7 @@ namespace Barotrauma
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
//or modify the affliction instance of an Attack or a StatusEffect
var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength),
newAffliction.Source);
afflictions.Add(copyAffliction, limbHealth);
AchievementManager.OnAfflictionReceived(copyAffliction, Character);

View File

@@ -190,7 +190,7 @@ namespace Barotrauma
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
humanAI.ReportRange = Hearing;
humanAI.Hearing = Hearing;
humanAI.ReportRange = ReportRange;
humanAI.FindWeaponsRange = FindWeaponsRange;
humanAI.AimSpeed = AimSpeed;

Some files were not shown because too many files have changed in this diff Show More