Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Evil Factory
2025-12-08 12:35:44 -03:00
122 changed files with 1614 additions and 819 deletions

View File

@@ -73,8 +73,7 @@ body:
label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options:
- v1.10.7.2 (Autumn Update 2025 Hotfix 4)
- v1.11.0.0 (Unstable)
- v1.11.4.1 (Winter Update 2025)
- Other
validations:
required: true

View File

@@ -9,7 +9,7 @@ namespace Barotrauma
{
public override void DebugDraw(SpriteBatch spriteBatch)
{
if (Character.IsUnconscious || !Character.Enabled || !Enabled) { return; }
if (Character.IsUnconscious || !Character.Enabled || !Enabled || Screen.Selected?.Cam is { Zoom: < 0.4f }) { return; }
Vector2 pos = Character.DrawPosition;
pos.Y = -pos.Y;

View File

@@ -11,7 +11,7 @@ namespace Barotrauma
public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
{
if (Character == Character.Controlled) { return; }
if (!DebugAI) { return; }
if (!DebugAI || Screen.Selected?.Cam is { Zoom: < 0.4f }) { return; }
Vector2 pos = Character.DrawPosition;
pos.Y = -pos.Y;
Vector2 textOffset = new Vector2(-40, -160);

View File

@@ -35,11 +35,7 @@ namespace Barotrauma
CharacterStateInfo serverPos = character.MemState.Last();
if (!character.isSynced)
{
SetPosition(serverPos.Position, lerp: false);
Collider.LinearVelocity = Vector2.Zero;
character.MemLocalState.Clear();
character.LastNetworkUpdateID = serverPos.ID;
character.isSynced = true;
SyncPosition(serverPos);
return;
}
@@ -198,11 +194,7 @@ namespace Barotrauma
if (!character.isSynced)
{
SetPosition(serverPos.Position, lerp: false);
Collider.LinearVelocity = Vector2.Zero;
character.MemLocalState.Clear();
character.LastNetworkUpdateID = serverPos.ID;
character.isSynced = true;
SyncPosition(serverPos);
return;
}
@@ -319,6 +311,15 @@ namespace Barotrauma
if (character.MemLocalState.Count > 120) { character.MemLocalState.RemoveRange(0, character.MemLocalState.Count - 120); }
character.MemState.Clear();
}
void SyncPosition(CharacterStateInfo serverPos)
{
SetPosition(serverPos.Position, lerp: false);
Collider.LinearVelocity = Vector2.Zero;
character.MemLocalState.Clear();
character.LastNetworkUpdateID = serverPos.ID;
character.isSynced = true;
}
}
/// <summary>
@@ -657,7 +658,7 @@ namespace Barotrauma
public void DebugDraw(SpriteBatch spriteBatch)
{
if (!GameMain.DebugDraw || !character.Enabled) { return; }
if (!GameMain.DebugDraw || !character.Enabled || Screen.Selected?.Cam is { Zoom: < 0.2f }) { return; }
if (simplePhysicsEnabled) { return; }
foreach (Limb limb in Limbs)

View File

@@ -54,15 +54,29 @@ namespace Barotrauma
get { return controlled; }
set
{
if (controlled == value) return;
if ((!(controlled is null)) && (!(Screen.Selected?.Cam is null)) && value is null)
if (controlled == value && controlled == null)
{
Screen.Selected.Cam.TargetPos = Vector2.Zero;
Lights.LightManager.ViewTarget = null;
// Return early, but only when setting the controlled to null, because on controlling a character, we'll want to ensure that the target is both enabled and unfrozen.
return;
}
if (controlled != value)
{
CharacterHealth.OpenHealthWindow = null;
if (controlled != null && value == null)
{
if (Screen.Selected?.Cam is Camera camera)
{
camera.TargetPos = Vector2.Zero;;
}
Lights.LightManager.ViewTarget = null;
}
}
controlled = value;
if (controlled != null) controlled.Enabled = true;
CharacterHealth.OpenHealthWindow = null;
if (controlled != null)
{
controlled.Enabled = true;
controlled.AnimController.Frozen = false;
}
}
}
@@ -442,7 +456,7 @@ namespace Barotrauma
{
cam.OffsetAmount = targetOffsetAmount = maxOffset;
}
else if (SelectedItem != null && ViewTarget == null &&
else if (SelectedItem != null && ViewTarget == null && !IsIncapacitated &&
SelectedItem.Components.Any(ic => ic?.GuiFrame != null && ic.ShouldDrawHUD(this)))
{
cam.OffsetAmount = targetOffsetAmount = 0.0f;
@@ -456,7 +470,7 @@ namespace Barotrauma
}
else if (Lights.LightManager.ViewTarget == this)
{
if (GUI.PauseMenuOpen || IsUnconscious)
if (GUI.PauseMenuOpen || IsIncapacitated)
{
if (deltaTime > 0.0f)
{

View File

@@ -23,8 +23,8 @@ namespace Barotrauma
memState.Clear();
return;
}
//freeze AI characters if more than x seconds have passed since last update from the server
//freeze other characters (than the controlled) if more than x seconds have passed since last update from the server
if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - NetConfig.FreezeCharacterIfPositionDataMissingDelay)
{
AnimController.Frozen = true;

View File

@@ -104,6 +104,8 @@ namespace Barotrauma
private GUILayoutGroup treatmentLayout;
private GUIListBox recommendedTreatmentContainer;
private LocalizedString prevHighlightedAfflictionDescription;
/// <summary>
/// Timer for updating visuals (limb tints and overlays) caused by the affliction
/// </summary>
@@ -120,7 +122,7 @@ namespace Barotrauma
private float updateDisplayedAfflictionsTimer;
private const float UpdateDisplayedAfflictionsInterval = 0.5f;
private List<Affliction> currentDisplayedAfflictions = new List<Affliction>();
private readonly List<Affliction> currentDisplayedAfflictions = new List<Affliction>();
public float DisplayedVitality, DisplayVitalityDelay;
@@ -662,7 +664,8 @@ namespace Barotrauma
else
{
forceAfflictionContainerUpdate = true;
currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null);
currentDisplayedAfflictions.Clear();
currentDisplayedAfflictions.AddRange(GetAllAfflictions(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null));
currentDisplayedAfflictions.Sort((a1, a2) =>
{
int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond);
@@ -1165,7 +1168,6 @@ namespace Barotrauma
statusIconVisibleTime[afflictionPrefab] += deltaTime;
Color color = GetAfflictionIconColor(afflictionPrefab, affliction);
var matchingIcon =
afflictionIconContainer.GetChildByUserData(afflictionPrefab) ??
hiddenAfflictionIconContainer.GetChildByUserData(afflictionPrefab);
@@ -1177,9 +1179,20 @@ namespace Barotrauma
ToolTip = $"‖color:{color.ToStringHex()}‖{affliction.Prefab.Name}‖color:end‖",
CanBeSelected = false
};
new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true)
{
CanBeFocused = false
};
}
if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > HideStatusIconDelay)
{
matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform;
}
else
{
if (affliction.Prefab.ShowDescriptionInTooltip)
{
matchingIcon.ToolTip = matchingIcon.ToolTip + "\n" + affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.Self);
matchingIcon.ToolTip = $"‖color:{color.ToStringHex()}‖{affliction.Prefab.Name}‖color:end‖" + "\n" + affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.Self);
}
if (affliction == pressureAffliction)
{
@@ -1191,14 +1204,6 @@ namespace Barotrauma
}
matchingIcon.ToolTip = RichString.Rich(matchingIcon.ToolTip);
new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true)
{
CanBeFocused = false
};
}
if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > HideStatusIconDelay)
{
matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform;
}
var image = matchingIcon.GetChild<GUIImage>();
image.Color = color;
@@ -1574,12 +1579,14 @@ namespace Barotrauma
CanBeFocused = false
};
prevHighlightedAfflictionDescription = affliction.Prefab.GetDescription(
affliction.Strength,
Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter);
var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform),
RichString.Rich(affliction.Prefab.GetDescription(
affliction.Strength,
Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter)),
RichString.Rich(prevHighlightedAfflictionDescription),
textAlignment: Alignment.TopLeft, wrap: true)
{
UserData = "description",
CanBeFocused = false
};
@@ -1724,7 +1731,6 @@ namespace Barotrauma
var labelContainer = parent.GetChildByUserData("label");
var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock;
strengthText.Text = affliction.GetStrengthText();
strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red,
@@ -1743,6 +1749,19 @@ namespace Barotrauma
vitalityText.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green :
Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength);
}
var newDescription =
affliction.Prefab.GetDescription(
affliction.Strength,
Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter);
if (newDescription != prevHighlightedAfflictionDescription)
{
if (parent.GetChildByUserData("description") is GUITextBlock descriptionText)
{
descriptionText.Text = RichString.Rich(newDescription);
}
prevHighlightedAfflictionDescription = newDescription;
}
}
public bool OnItemDropped(Item item, bool ignoreMousePos)

View File

@@ -1,78 +1,48 @@
using Microsoft.Xna.Framework;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
partial class MissionPrefab : PrefabWithUintIdentifier
internal sealed partial class MissionPrefab : PrefabWithUintIdentifier
{
private ImmutableArray<Sprite> portraits = new ImmutableArray<Sprite>();
private ImmutableArray<Sprite> portraits = [];
public bool HasPortraits => portraits.Length > 0;
public Sprite Icon
{
get;
private set;
}
public Sprite Icon { get; private set; }
public Color IconColor { get; private set; }
public Color IconColor
{
get;
private set;
}
public bool DisplayTargetHudIcons
{
get;
private set;
}
public float HudIconMaxDistance
{
get;
private set;
}
public Sprite HudIcon
{
get
{
return hudIcon ?? Icon;
}
}
public Color HudIconColor
{
get
{
return hudIconColor ?? IconColor;
}
}
public Color ProgressBarColor { get; private set; }
public bool DisplayTargetHudIcons { get; private set; }
public float HudIconMaxDistance { get; private set; }
private Sprite hudIcon;
public Sprite HudIcon => hudIcon ?? Icon;
private Color? hudIconColor;
public Color HudIconColor => hudIconColor ?? IconColor;
public Color ProgressBarColor { get; private set; }
private ImmutableDictionary<int, Identifier> overrideMusicOnState;
partial void InitProjSpecific(ContentXElement element)
private void ParseConfigElementClient(ContentXElement element, MissionPrefab variantOf = null)
{
DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false);
HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f);
Dictionary<int, Identifier> overrideMusic = new Dictionary<int, Identifier>();
List<Sprite> portraits = new List<Sprite>();
foreach (var subElement in element.Elements())
HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000f);
Dictionary<int, Identifier> overrideMusic = [];
List<Sprite> portraits = [];
foreach (ContentXElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "icon":
Icon = new Sprite(subElement);
Icon = new Sprite(subElement, GetTexturePath(subElement, variantOf));
IconColor = subElement.GetAttributeColor("color", Color.White);
break;
case "hudicon":
hudIcon = new Sprite(subElement);
hudIcon = new Sprite(subElement, GetTexturePath(subElement, variantOf));
hudIconColor = subElement.GetAttributeColor("color");
break;
case "overridemusic":
@@ -81,7 +51,7 @@ namespace Barotrauma
subElement.GetAttributeIdentifier("type", Identifier.Empty));
break;
case "portrait":
var portrait = new Sprite(subElement, lazyLoad: true);
Sprite portrait = new(subElement, GetTexturePath(subElement, variantOf), lazyLoad: true);
if (portrait != null)
{
portraits.Add(portrait);
@@ -89,7 +59,7 @@ namespace Barotrauma
break;
}
}
this.portraits = portraits.ToImmutableArray();
this.portraits = [.. portraits];
overrideMusicOnState = overrideMusic.ToImmutableDictionary();
ProgressBarColor = element.GetAttributeColor(nameof(ProgressBarColor), GUIStyle.Blue);
}
@@ -109,6 +79,11 @@ namespace Barotrauma
return portraits[Math.Abs(randomSeed) % portraits.Length];
}
public string GetTexturePath(ContentXElement subElement, MissionPrefab variantOf = null)
=> subElement.DoesAttributeReferenceFileNameAlone("texture")
? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path)
: "";
partial void DisposeProjectSpecific()
{
Icon?.Remove();

View File

@@ -3338,7 +3338,13 @@ namespace Barotrauma
if (!CanIssueOrders) { return false; }
var character = userData as Character;
int priority = GetManualOrderPriority(character, order);
SetCharacterOrder(character, order.WithManualPriority(priority).WithOrderGiver(Character.Controlled));
Item targetEntity = null;
if (order.MustSetTarget && order.TargetEntity == null)
{
var matchingItems = order.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: characterContext ?? Character.Controlled);
targetEntity = matchingItems.FirstOrDefault();
}
SetCharacterOrder(character, order.WithItemComponent(targetEntity).WithManualPriority(priority).WithOrderGiver(Character.Controlled));
DisableCommandUI();
return true;
}

View File

@@ -93,7 +93,7 @@ namespace Barotrauma
public override void ShowStartMessage()
{
foreach (Mission mission in Missions.ToList())
foreach (Mission mission in Missions.OrderBy(m => m.Prefab.IsSideObjective).ToList())
{
if (!mission.Prefab.ShowStartMessage) { continue; }
new GUIMessageBox(

View File

@@ -112,8 +112,16 @@ namespace Barotrauma
GUITextBlock.AutoScaleAndNormalize(newCampaignButton.TextBlock, loadCampaignButton.TextBlock);
GameMain.NetLobbyScreen.CampaignSetupUI.StartNewGame = GameMain.Client.SetupNewCampaign;
GameMain.NetLobbyScreen.CampaignSetupUI.LoadGame = GameMain.Client.SetupLoadCampaign;
GameMain.NetLobbyScreen.CampaignSetupUI.StartNewGame = (SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings) =>
{
GameMain.NetLobbyScreen.SetAFKSelected(false);
GameMain.Client.SetupNewCampaign(sub, saveName, mapSeed, settings);
};
GameMain.NetLobbyScreen.CampaignSetupUI.LoadGame = (string filePath, Option<uint> backupIndex) =>
{
GameMain.NetLobbyScreen.SetAFKSelected(false);
GameMain.Client.SetupLoadCampaign(filePath, backupIndex);
};
}
partial void InitProjSpecific()
@@ -169,6 +177,7 @@ namespace Barotrauma
{
StartRound = () =>
{
GameMain.NetLobbyScreen.SetAFKSelected(false);
GameMain.Client.RequestStartRound();
}
};

View File

@@ -1,9 +1,9 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -22,6 +22,9 @@ namespace Barotrauma
private GUIButton createEventButton;
public LevelGenerationParams BackgroundParams { get; private set; }
public Vector2 WaterParticleOffset;
public override void Start()
{
base.Start();
@@ -68,6 +71,12 @@ namespace Barotrauma
}
};
}
if (Level.Loaded == null)
{
BackgroundParams ??= LevelGenerationParams.LevelParams.Where(lp => !lp.AllowedBiomeIdentifiers.Contains("endzone")).GetRandom(Rand.RandSync.Unsynced);
GameMain.LightManager.AmbientLight = BackgroundParams.AmbientLightColor;
}
}
public override void AddToGUIUpdateList()
@@ -93,6 +102,8 @@ namespace Barotrauma
sEvent.Update(deltaTime);
}
}
BackgroundParams?.UpdateWaterParticleOffset(ref WaterParticleOffset, BackgroundParams.WaterParticleVelocity, deltaTime);
}
private void GenerateOutpost(Submarine submarine)

View File

@@ -173,7 +173,8 @@ namespace Barotrauma
List<Mission> missionsToDisplay = new List<Mission>(selectedMissions.Where(m => m.Prefab.ShowInMenus));
if (startLocation != null)
{
foreach (Mission mission in startLocation.SelectedMissions)
//side objectives can't be selected manually (they're always selected), we need to add them separately from SelectedMissions
foreach (Mission mission in startLocation.SelectedMissions.Union(startLocation.AvailableMissions.Where(m => m.Prefab.IsSideObjective)))
{
if (missionsToDisplay.Contains(mission)) { continue; }
if (!mission.Prefab.ShowInMenus) { continue; }

View File

@@ -119,8 +119,7 @@ namespace Barotrauma.Items.Components
if (!attached)
{
Drop(false, null);
item.SetTransform(simPosition, 0.0f);
item.Submarine = sub;
item.SetTransform(simPosition, 0.0f, forceSubmarine: sub);
AttachToWall();
PlaySound(ActionType.OnUse, attacher);
ApplyStatusEffects(ActionType.OnUse, (float)Timing.Step, character: attacher, user: attacher);
@@ -142,8 +141,7 @@ namespace Barotrauma.Items.Components
}
else
{
item.SetTransform(simPosition, 0.0f);
item.Submarine = sub;
item.SetTransform(simPosition, 0.0f, forceSubmarine: sub);
}
}
}

View File

@@ -28,8 +28,8 @@ namespace Barotrauma.Items.Components
set
{
_chargeSoundWindupPitchSlide = new Vector2(
Math.Max(value.X, SoundChannel.MinFrequencyMultiplier),
Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier));
MathHelper.Clamp(value.X, SoundChannel.MinFrequencyMultiplier, SoundChannel.MaxFrequencyMultiplier),
MathHelper.Clamp(value.Y, SoundChannel.MinFrequencyMultiplier, SoundChannel.MaxFrequencyMultiplier));
}
}
private Vector2 _chargeSoundWindupPitchSlide;

View File

@@ -409,10 +409,10 @@ namespace Barotrauma.Items.Components
loopingSoundChannel.Looping = true;
loopingSoundChannel.Near = loopingSound.Range * 0.4f;
loopingSoundChannel.Far = loopingSound.Range;
}
if (loopingSound.RoundSound.Stream)
{
loopingSoundChannel.StreamSeekPos = loopingSound.RoundSound.LastStreamSeekPos;
if (loopingSound.RoundSound.Stream)
{
loopingSoundChannel.StreamSeekPos = loopingSound.RoundSound.LastStreamSeekPos;
}
}
}
}

View File

@@ -434,12 +434,10 @@ namespace Barotrauma.Items.Components
foreach (FabricationRecipe fi in fabricationRecipes.Values)
{
RichString recipeTooltip = RichString.Rich(fi.TargetItem.Description);
if (fi.RequiresRecipe)
{
recipeTooltip += "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖";
}
recipeTooltip = RichString.Rich(recipeTooltip);
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)
{
@@ -894,10 +892,11 @@ namespace Barotrauma.Items.Components
if (outputContainer.Inventory.IsEmpty())
{
var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.Sprite;
Color iconColor = itemIcon == targetItem.TargetItem.Sprite ? targetItem.TargetItem.SpriteColor : targetItem.TargetItem.InventoryIconColor;
itemIcon.Draw(
spriteBatch,
slotRect.Center.ToVector2(),
color: Color.Lerp(targetItem.TargetItem.InventoryIconColor, Color.TransparentBlack, 0.5f),
color: Color.Lerp(iconColor, Color.TransparentBlack, 0.5f),
scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y) * 0.9f);
}
}
@@ -1132,24 +1131,27 @@ namespace Barotrauma.Items.Components
if (!selectedRecipe.TargetItem.Description.IsNullOrEmpty())
{
RichString richDescription = RichString.Rich(selectedRecipe.TargetItem.Description);
var descriptionParent = largeUI ? paddedReqFrame : paddedFrame;
var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionParent.RectTransform),
RichString.Rich(selectedRecipe.TargetItem.Description),
richDescription,
font: GUIStyle.SmallFont, wrap: true);
if (!largeUI)
{
description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W);
}
while (description.Rect.Height + nameBlock.Rect.Height > descriptionParent.Rect.Height)
while (description.Rect.Height + nameBlock.Rect.Height > descriptionParent.Rect.Height / 2)
{
var lines = description.WrappedText.Split('\n');
if (lines.Count <= 1) { break; }
var newString = string.Join('\n', lines.Take(lines.Count - 1));
string newString = string.Join('\n', lines.Take(lines.Count - 1));
description.Text = newString.Substring(0, newString.Length - 4) + "...";
description.CalculateHeightFromText();
description.ToolTip = selectedRecipe.TargetItem.Description;
description.ToolTip = richDescription;
}
description.Text.RetrieveValue();
}
IEnumerable<Skill> inadequateSkills = Enumerable.Empty<Skill>();
@@ -1328,7 +1330,10 @@ namespace Barotrauma.Items.Components
var childContainer = child.GetChild<GUILayoutGroup>();
childContainer.GetChild<GUITextBlock>().TextColor = baseColor * (canBeFabricated ? 1.0f : 0.5f);
childContainer.GetChild<GUIImage>().Color = recipe.TargetItem.InventoryIconColor * (canBeFabricated ? 1.0f : 0.5f);
GUIImage icon = childContainer.GetChild<GUIImage>();
Color iconColor = icon.Sprite == recipe.TargetItem.Sprite ? recipe.TargetItem.SpriteColor : recipe.TargetItem.InventoryIconColor;
childContainer.GetChild<GUIImage>().Color = iconColor * (canBeFabricated ? 1.0f : 0.5f);
var limitReachedText = child.FindChild(nameof(FabricationLimitReachedText));
limitReachedText.Visible = !canBeFabricated && fabricationLimits.TryGetValue(recipe.RecipeHash, out int amount) && amount <= 0;

View File

@@ -137,46 +137,30 @@ namespace Barotrauma.Items.Components
{
foreach (var (position, emitter) in pumpOutEmitters)
{
if (item.CurrentHull != null && item.CurrentHull.Surface < item.Rect.Location.Y + position.Y) { continue; }
//only emit "pump out" particles when underwater
Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition;
relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad);
float angle = -item.RotationRad;
if (item.FlippedX)
{
relativeParticlePos.X = -relativeParticlePos.X;
angle += MathHelper.Pi;
}
if (item.FlippedY)
{
relativeParticlePos.Y = -relativeParticlePos.Y;
}
emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle,
velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, -currFlow / maxFlow));
if (item.CurrentHull != null && item.CurrentHull.Surface < item.Rect.Location.Y + position.Y) { continue; }
Emit(position, emitter);
}
}
else if (currFlow > 0f)
{
foreach (var (position, emitter) in pumpInEmitters)
{
Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition;
relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? item.RotationRad : -item.RotationRad);
float angle = -item.RotationRad;
if (item.FlippedX)
{
relativeParticlePos.X = -relativeParticlePos.X;
angle += MathHelper.Pi;
}
if (item.FlippedY)
{
relativeParticlePos.Y = -relativeParticlePos.Y;
}
emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle,
velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, currFlow / maxFlow));
Emit(position, emitter);
}
}
void Emit(Vector2 position, ParticleEmitter emitter)
{
Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition;
if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; }
if (item.FlippedY) { relativeParticlePos.Y = -relativeParticlePos.Y; }
relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, -item.RotationRad);
float angle = -item.RotationRad;
if (item.FlippedX) { angle += MathHelper.Pi; }
emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle,
velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, currFlow / maxFlow), mirrorAngle: item.FlippedX ^ item.FlippedY);
}
}
private float flickerTimer;

View File

@@ -1406,7 +1406,8 @@ namespace Barotrauma.Items.Components
public void RegisterExplosion(Explosion explosion, Vector2 worldPosition)
{
if (Character.Controlled?.SelectedItem != item) { return; }
if (Character.Controlled?.SelectedItem == null) { return; }
if (Character.Controlled.SelectedItem != Item && !Character.Controlled.SelectedItem.linkedTo.Contains(Item)) { return; }
if (explosion.Attack.StructureDamage <= 0 && explosion.Attack.ItemDamage <= 0 && explosion.EmpStrength <= 0) { return; }
Vector2 transducerCenter = GetTransducerPos();
if (Vector2.DistanceSquared(worldPosition, transducerCenter) > range * range) { return; }
@@ -1564,7 +1565,15 @@ namespace Barotrauma.Items.Components
foreach (Item item in Item.SonarVisibleItems)
{
System.Diagnostics.Debug.Assert(item.Prefab.SonarSize > 0.0f);
if (item.CurrentHull == null)
if (item.CurrentHull != null) { continue; }
bool isItemVisible =
item.ParentInventory == null ||
item.GetComponent<Holdable>() is { IsActive: true } ||
item.Container?.GetComponent<ItemContainer>() is { HideItems: false };
if (isItemVisible)
{
float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared();
if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)

View File

@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
int connectorIntervalLeft = GetConnectorIntervalLeft(height, panel);
int connectorIntervalRight = GetConnectorIntervalRight(height, panel);
foreach (Connection c in panel.Connections)
foreach (Connection c in panel.Connections.OrderBy(static c => c.DisplayOrder))
{
//if dragging a wire, let the Inventory know so that the wire can be
//dropped or dragged from the panel to the players inventory
@@ -192,7 +192,7 @@ namespace Barotrauma.Items.Components
{
DrawWire(spriteBatch, equippedWire, new Vector2(x + width / 2, y + height - 150 * GUI.Scale),
new Vector2(x + width / 2, y + height),
null, panel, "");
null, panel, label: GetWireLabel(connection: null, wire: equippedWire));
if (DraggingConnected == equippedWire)
{
@@ -209,13 +209,9 @@ namespace Barotrauma.Items.Components
{
if (wire == DraggingConnected && mouseInRect) { continue; }
if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; }
Connection recipient = wire.OtherConnection(null);
LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})";
if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); }
DrawWire(spriteBatch, wire, new Vector2(x, y + height - 100 * GUI.Scale),
new Vector2(x, y + height),
null, panel, label);
null, panel, label: GetWireLabel(connection: null, wire));
x += (int)step;
}
@@ -290,21 +286,7 @@ namespace Barotrauma.Items.Components
{
if (wire.Hidden || (DraggingConnected == wire && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; }
if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; }
Connection recipient = wire.OtherConnection(this);
LocalizedString label;
if (wire.Item.IsLayerHidden)
{
label = TextManager.Get("ConnectionLocked");
}
else
{
label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})";
if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); }
}
DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label);
DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label: GetWireLabel(connection: this, wire: wire));
wirePosition.Y += wireInterval;
}
@@ -362,6 +344,22 @@ namespace Barotrauma.Items.Components
}
}
private static LocalizedString GetWireLabel(Connection connection, Wire wire)
{
Connection recipient = wire.OtherConnection(connection);
LocalizedString label;
if (wire.Item.IsLayerHidden)
{
label = TextManager.Get("ConnectionLocked");
}
else
{
label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})";
if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); }
}
return label;
}
public void Flash(Color? color = null, float flashDuration = 1.5f)
{
FlashTimer = flashDuration;

View File

@@ -0,0 +1,12 @@
using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
partial class ConnectionSelectorComponent : ItemComponent
{
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
SelectedConnection = msg.ReadRangedInteger(0, 255);
}
}
}

View File

@@ -733,7 +733,7 @@ namespace Barotrauma
{
updateableComponents.Add(ic);
}
isActive = true;
IsActive = true;
}
}
@@ -2170,7 +2170,7 @@ namespace Barotrauma
return;
}
isActive = true;
IsActive = true;
if (positionBuffer.Count > 0)
{
@@ -2515,8 +2515,7 @@ namespace Barotrauma
{
inventory.TryPutItem(item, user: null, allowedSlots: item.AllowedSlots, createNetworkEvent: false);
}
item.SetTransform(inventory.Owner.SimPosition, 0.0f);
item.Submarine = inventory.Owner.Submarine;
item.SetTransform(inventory.Owner.SimPosition, 0.0f, forceSubmarine: inventory.Owner.Submarine);
if (inventory.Owner is Character { Enabled: false } && item.body != null)
{
item.body.Enabled = false;

View File

@@ -230,7 +230,12 @@ namespace Barotrauma
if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld && !doubleClicked && !secondaryDoubleClicked) { return; }
Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition);
Hull hull = FindHull(position);
Hull hull =
Screen.Selected is { IsEditor: true } ?
//use the unoptimized version in the editor to make sure newly placed hulls are found
//(FindHull uses the "EntityGrid" which is generated after loading the sub)
FindHullUnoptimized(position) :
FindHull(position);
if (hull == null || hull.IdFreed) { return; }
if (EditWater)
@@ -361,7 +366,7 @@ namespace Barotrauma
new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height),
GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(MathF.Ceiling(1.5f / Screen.Selected.Cam.Zoom), 1.0f));
if (GameMain.DebugDraw)
if (GameMain.DebugDraw && Screen.Selected?.Cam is { Zoom: > 0.5f })
{
GUIStyle.SmallFont.DrawString(spriteBatch, "Pressure: " + ((int)pressure - rect.Y).ToString() +
" - Oxygen: " + ((int)OxygenPercentage), new Vector2(drawRect.X + 5, -drawRect.Y + 5), Color.White);

View File

@@ -0,0 +1,74 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
namespace Barotrauma;
internal partial class LevelGenerationParams : PrefabWithUintIdentifier
{
/// <remarks>Doesn't call SpriteBatch.Begin and SpriteBatch.End; They must be called manually.</remarks>
public void DrawBackgrounds(SpriteBatch spriteBatch, Camera cam)
{
if (BackgroundTopSprite == null) { return; }
Vector2 backgroundPos = cam.WorldViewCenter.FlipY() * 0.05f;
int backgroundSize = (int)BackgroundTopSprite.size.Y;
if (backgroundPos.Y >= backgroundSize) { return; }
if (backgroundPos.Y < 0f)
{
BackgroundTopSprite.SourceRect = new Rectangle((int)backgroundPos.X, (int)backgroundPos.Y, backgroundSize, (int)Math.Min(-backgroundPos.Y, backgroundSize));
BackgroundTopSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, Math.Min(-backgroundPos.Y, GameMain.GraphicsHeight)),
color: BackgroundTextureColor);
}
if (-backgroundPos.Y < GameMain.GraphicsHeight && BackgroundSprite != null)
{
BackgroundSprite.SourceRect = new Rectangle((int)backgroundPos.X, (int)Math.Max(backgroundPos.Y, 0), backgroundSize, backgroundSize);
BackgroundSprite.DrawTiled(spriteBatch, (backgroundPos.Y < 0f) ? new Vector2(0f, (int)-backgroundPos.Y) : Vector2.Zero,
new Vector2(GameMain.GraphicsWidth, (int)Math.Min(Math.Ceiling(backgroundSize - backgroundPos.Y), backgroundSize)),
color: BackgroundTextureColor);
}
}
/// <remarks>Doesn't call SpriteBatch.Begin and SpriteBatch.End; They must be called manually.</remarks>
public void DrawWaterParticles(SpriteBatch spriteBatch, Camera cam, Vector2 offset)
{
if (WaterParticles == null || cam.Zoom <= 0.05f) { return; }
float textureScale = WaterParticleScale;
Vector2 textureSize = new Vector2(WaterParticles.Texture.Width, WaterParticles.Texture.Height);
Vector2 origin = new Vector2(cam.WorldView.X, -cam.WorldView.Y);
offset -= origin;
// Draw 4 layers of particles.
for (int i = 0; i < 4; i++)
{
float scale = 1f - i * 0.2f;
float alpha = MathUtils.InverseLerp(0.05f, 0.1f, cam.Zoom * scale);
if (alpha == 0f) { continue; }
Vector2 newOffset = offset * scale;
newOffset += cam.WorldView.Size.ToVector2() * (1f - scale) * 0.5f;
newOffset -= new Vector2(256f * i);
float newTextureScale = scale * textureScale;
Vector2 newSize = textureSize * scale;
while (newOffset.X <= -newSize.X) { newOffset.X += newSize.X; }
while (newOffset.X > 0f) { newOffset.X -= newSize.X; }
while (newOffset.Y <= -newSize.Y) { newOffset.Y += newSize.Y; }
while (newOffset.Y > 0f) { newOffset.Y -= newSize.Y; }
WaterParticles.DrawTiled(spriteBatch, origin + newOffset, cam.WorldView.Size.ToVector2() - newOffset,
color: WaterParticleColor * alpha, textureScale: new Vector2(newTextureScale));
}
}
public void UpdateWaterParticleOffset(ref Vector2 offset, Vector2 velocity, float deltaTime)
{
if (WaterParticles == null) { return; }
Vector2 waterTextureSize = WaterParticles.size * WaterParticleScale;
offset += velocity.FlipY() * WaterParticleScale * deltaTime;
offset.X %= waterTextureSize.X;
offset.Y %= waterTextureSize.Y;
}
}

View File

@@ -234,15 +234,7 @@ namespace Barotrauma
WaterRenderer.Instance?.ScrollWater(waterParticleVelocity, deltaTime);
if (level.GenerationParams.WaterParticles != null)
{
Vector2 waterTextureSize = level.GenerationParams.WaterParticles.size * level.GenerationParams.WaterParticleScale;
waterParticleOffset += new Vector2(waterParticleVelocity.X, -waterParticleVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime;
while (waterParticleOffset.X <= -waterTextureSize.X) { waterParticleOffset.X += waterTextureSize.X; }
while (waterParticleOffset.X >= waterTextureSize.X){ waterParticleOffset.X -= waterTextureSize.X; }
while (waterParticleOffset.Y <= -waterTextureSize.Y) { waterParticleOffset.Y += waterTextureSize.Y; }
while (waterParticleOffset.Y >= waterTextureSize.Y) { waterParticleOffset.Y -= waterTextureSize.Y; }
}
level.GenerationParams.UpdateWaterParticleOffset(ref waterParticleOffset, waterParticleVelocity, deltaTime);
}
public static VertexPositionColorTexture[] GetColoredVertices(VertexPositionTexture[] vertices, Color color)
@@ -274,36 +266,7 @@ namespace Barotrauma
ParticleManager particleManager = null)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap);
Vector2 backgroundPos = cam.WorldViewCenter;
backgroundPos.Y = -backgroundPos.Y;
backgroundPos *= 0.05f;
if (level.GenerationParams.BackgroundTopSprite != null)
{
int backgroundSize = (int)level.GenerationParams.BackgroundTopSprite.size.Y;
if (backgroundPos.Y < backgroundSize)
{
if (backgroundPos.Y < 0)
{
var backgroundTop = level.GenerationParams.BackgroundTopSprite;
backgroundTop.SourceRect = new Rectangle((int)backgroundPos.X, (int)backgroundPos.Y, backgroundSize, (int)Math.Min(-backgroundPos.Y, backgroundSize));
backgroundTop.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, Math.Min(-backgroundPos.Y, GameMain.GraphicsHeight)),
color: level.BackgroundTextureColor);
}
if (-backgroundPos.Y < GameMain.GraphicsHeight && level.GenerationParams.BackgroundSprite != null)
{
var background = level.GenerationParams.BackgroundSprite;
background.SourceRect = new Rectangle((int)backgroundPos.X, (int)Math.Max(backgroundPos.Y, 0), backgroundSize, backgroundSize);
background.DrawTiled(spriteBatch,
(backgroundPos.Y < 0) ? new Vector2(0.0f, (int)-backgroundPos.Y) : Vector2.Zero,
new Vector2(GameMain.GraphicsWidth, (int)Math.Min(Math.Ceiling(backgroundSize - backgroundPos.Y), backgroundSize)),
color: level.BackgroundTextureColor);
}
}
}
level.GenerationParams.DrawBackgrounds(spriteBatch, cam);
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred,
@@ -317,42 +280,7 @@ namespace Barotrauma
backgroundCreatureManager?.Draw(spriteBatch, cam);
}
if (level.GenerationParams.WaterParticles != null && cam.Zoom > 0.05f)
{
float textureScale = level.GenerationParams.WaterParticleScale;
Rectangle srcRect = new Rectangle(0, 0, 2048, 2048);
Vector2 origin = new Vector2(cam.WorldView.X, -cam.WorldView.Y);
Vector2 offset = -origin + waterParticleOffset;
while (offset.X <= -srcRect.Width * textureScale) offset.X += srcRect.Width * textureScale;
while (offset.X > 0.0f) offset.X -= srcRect.Width * textureScale;
while (offset.Y <= -srcRect.Height * textureScale) offset.Y += srcRect.Height * textureScale;
while (offset.Y > 0.0f) offset.Y -= srcRect.Height * textureScale;
for (int i = 0; i < 4; i++)
{
float scale = (1.0f - i * 0.2f);
//alpha goes from 1.0 to 0.0 when scale is in the range of 0.1 - 0.05
float alpha = (cam.Zoom * scale) < 0.1f ? (cam.Zoom * scale - 0.05f) * 20.0f : 1.0f;
if (alpha <= 0.0f) continue;
Vector2 offsetS = offset * scale
+ new Vector2(cam.WorldView.Width, cam.WorldView.Height) * (1.0f - scale) * 0.5f
- new Vector2(256.0f * i);
float texScale = scale * textureScale;
while (offsetS.X <= -srcRect.Width * texScale) offsetS.X += srcRect.Width * texScale;
while (offsetS.X > 0.0f) offsetS.X -= srcRect.Width * texScale;
while (offsetS.Y <= -srcRect.Height * texScale) offsetS.Y += srcRect.Height * texScale;
while (offsetS.Y > 0.0f) offsetS.Y -= srcRect.Height * texScale;
level.GenerationParams.WaterParticles.DrawTiled(
spriteBatch, origin + offsetS,
new Vector2(cam.WorldView.Width - offsetS.X, cam.WorldView.Height - offsetS.Y),
color: level.GenerationParams.WaterParticleColor * alpha, textureScale: new Vector2(texScale));
}
}
level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset);
GameMain.ParticleManager?.Draw(spriteBatch, inWater: true, inSub: false, ParticleBlendState.AlphaBlend, background: true);

View File

@@ -89,7 +89,7 @@ namespace Barotrauma.Lights
}
private float pulseAmount;
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")]
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates. 0 = not at all, 1 = alternates between full brightness and off.")]
public float PulseAmount
{
get { return pulseAmount; }

View File

@@ -397,7 +397,7 @@ namespace Barotrauma
}
if (!description.IsNullOrEmpty())
{
CreateTextWithIcon(description, locationTypeToDisplay.Sprite);
CreateTextWithIcon(description, iconSprite: locationTypeToDisplay.Sprite);
}
int highestSubTier = location.HighestSubmarineTierAvailable();
@@ -417,32 +417,29 @@ namespace Barotrauma
}
if (highestSubTier > 0)
{
CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon");
CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), iconStyle: "LocationOverlaySubmarineIcon");
}
if (overrideTiers != null)
{
foreach (var (subClass, tier) in overrideTiers)
{
CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon");
CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), iconStyle: "LocationOverlaySubmarineIcon");
}
}
CreateSpacing(10);
void CreateTextWithIcon(LocalizedString text, Sprite icon, string style = null)
void CreateTextWithIcon(LocalizedString text, Sprite iconSprite = null, string iconStyle = null)
{
var textHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, (int)GUIStyle.Font.MeasureString(text).Y), content.RectTransform), isHorizontal: true)
{
Stretch = true,
CanBeFocused = true
};
var guiIcon =
style == null ?
new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon) :
new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style);
var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1.0f), textHolder.RectTransform), text);
textBlock.RectTransform.MinSize = new Point((int)textBlock.TextSize.X, 0);
textHolder.RectTransform.MinSize = new Point((int)textBlock.TextSize.X + guiIcon.Rect.Width, 0);
GUITextBlock textBox = new(new RectTransform(new Vector2(0.9f, 0f), content.RectTransform), text, wrap: true);
if (iconSprite == null && iconStyle == null) { return; }
float iconSize = GUIStyle.Font.LineHeight * 1.5f;
textBox.Padding = textBox.Padding with { X = iconSize + 5f };
RectTransform iconTF = new(new Point((int)iconSize), textBox.RectTransform, Anchor.CenterLeft) { IsFixedSize = true };
if (iconSprite != null) { new GUIImage(iconTF, iconSprite, scaleToFit: true); }
if (iconStyle != null) { new GUIImage(iconTF, iconStyle, scaleToFit: true); }
}
void CreateSpacing(int height)
@@ -486,10 +483,11 @@ namespace Barotrauma
CreateSpacing(20);
}
locationInfoOverlay.RectTransform.NonScaledSize =
new Point(
Math.Max(locationInfoOverlay.Rect.Width, (int)(content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X : c.RectTransform.MinSize.X) * 1.2f)),
(int)(content.Children.Sum(c => c.Rect.Height) / content.RectTransform.RelativeSize.Y));
float childWidth = Math.Max(locationInfoOverlay.Rect.Width, content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X + textBlock.Padding.X + textBlock.Padding.Z : c.RectTransform.MinSize.X));
childWidth = Math.Max(locationInfoOverlay.Rect.Width, childWidth);
float childHeight = content.Children.Sum(c => c.Rect.Height);
Vector2 childSize = new Vector2(childWidth, childHeight) / content.RectTransform.RelativeSize;
locationInfoOverlay.RectTransform.NonScaledSize = childSize.ToPoint();
}
partial void ClearAnimQueue()

View File

@@ -34,6 +34,8 @@ namespace Barotrauma
//which entities have been selected for editing
public static HashSet<MapEntity> SelectedList { get; private set; } = new HashSet<MapEntity>();
private static List<Rectangle> oldRects = new List<Rectangle>();
private static Vector2 entityMovementNudge;
public static List<MapEntity> CopiedList = new List<MapEntity>();
@@ -267,7 +269,7 @@ namespace Barotrauma
i++;
}
highlightedEntities.Insert(i, e);
if (i == 0) highLightedEntity = e;
if (i == 0) { highLightedEntity = e; }
}
}
UpdateHighlighting(highlightedEntities);
@@ -278,10 +280,19 @@ namespace Barotrauma
if (GUI.KeyboardDispatcher.Subscriber == null)
{
Vector2 nudge = GetNudgeAmount();
if (nudge != Vector2.Zero)
Vector2 previousNudge = entityMovementNudge;
entityMovementNudge = GetNudgeAmount();
if (entityMovementNudge != Vector2.Zero)
{
foreach (MapEntity entityToNudge in SelectedList) { entityToNudge.Move(nudge); }
if (previousNudge == Vector2.Zero)
{
oldRects = SelectedList.Select(entity => entity.Rect).ToList();
}
foreach (MapEntity entityToNudge in SelectedList) { entityToNudge.Move(entityMovementNudge); }
}
else if (previousNudge != Vector2.Zero)
{
SubEditorScreen.StoreCommand(new TransformCommand(new List<MapEntity>(SelectedList), SelectedList.Select(entity => entity.Rect).ToList(), oldRects, resized: false));
}
}
else
@@ -352,7 +363,7 @@ namespace Barotrauma
}
}
SubEditorScreen.StoreCommand(new TransformCommand(new List<MapEntity>(SelectedList),SelectedList.Select(entity => entity.Rect).ToList(), oldRects, false));
SubEditorScreen.StoreCommand(new TransformCommand(new List<MapEntity>(SelectedList), SelectedList.Select(entity => entity.Rect).ToList(), oldRects, resized: false));
if (deposited.Any() && deposited.Any(entity => entity is Item))
{
var depositedItems = deposited.Where(entity => entity is Item).Cast<Item>().ToList();
@@ -508,7 +519,7 @@ namespace Barotrauma
selectionSize = Vector2.Zero;
selectionPos = Vector2.Zero;
}
public static Vector2 GetNudgeAmount(bool doHold = true)
{
Vector2 nudgeAmount = Vector2.Zero;
@@ -532,10 +543,10 @@ namespace Barotrauma
}
}
if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f;
if (PlayerInput.KeyHit(Keys.Down)) nudgeAmount.Y = -1f;
if (PlayerInput.KeyHit(Keys.Left)) nudgeAmount.X = -1f;
if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f;
if (PlayerInput.KeyHit(Keys.Up)) { nudgeAmount.Y = 1f; }
if (PlayerInput.KeyHit(Keys.Down)) { nudgeAmount.Y = -1f; }
if (PlayerInput.KeyHit(Keys.Left)) { nudgeAmount.X = -1f;}
if (PlayerInput.KeyHit(Keys.Right)) { nudgeAmount.X = 1f; }
return nudgeAmount;
}

View File

@@ -172,16 +172,19 @@ namespace Barotrauma
GUI.DrawRectangle(spriteBatch, drawPos - ExitPointSize.ToVector2() / 2, ExitPointSize.ToVector2(), Color.Cyan, thickness: 5);
}
GUIStyle.SmallFont.DrawString(spriteBatch,
ID.ToString(),
new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30),
color);
if (Tunnel?.Type != null)
if (Screen.Selected?.Cam is { Zoom: > 0.4f })
{
GUIStyle.SmallFont.DrawString(spriteBatch,
Tunnel.Type.ToString(),
new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45),
color);
ID.ToString(),
new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30),
color);
if (Tunnel?.Type != null)
{
GUIStyle.SmallFont.DrawString(spriteBatch,
Tunnel.Type.ToString(),
new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45),
color);
}
}
}

View File

@@ -239,6 +239,13 @@ namespace Barotrauma.Networking
}
return;
}
//if we're still downloading mods, we're not ready to receive the campaign save
if (fileType == (byte)FileTransferType.CampaignSave && Screen.Selected is ModDownloadScreen)
{
GameMain.Client.CancelFileTransfer(transferId);
return;
}
if (!ValidateInitialData(fileType, fileName, fileSize, out string errorMsg))
{

View File

@@ -562,6 +562,11 @@ namespace Barotrauma.Networking
{
SendLobbyUpdate();
}
if (Timing.TotalTime > LastMissingCampaignSubRequestTime)
{
TryRequestMissingCampaignSubs();
LastMissingCampaignSubRequestTime = Timing.TotalTime + MissingCampaignSubRequestInterval;
}
}
if (ServerSettings.VoiceChatEnabled)
@@ -2284,13 +2289,7 @@ namespace Barotrauma.Networking
if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); }
}
if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
{
foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name)))
{
GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign);
}
}
TryRequestMissingCampaignSubs();
GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating);
GameMain.NetLobbyScreen.SetAllowAFK(allowAFK);
@@ -2646,6 +2645,21 @@ namespace Barotrauma.Networking
ClientPeer?.Send(msg, DeliveryMethod.Reliable);
}
private double LastMissingCampaignSubRequestTime;
const double MissingCampaignSubRequestInterval = 10.0f;
private void TryRequestMissingCampaignSubs()
{
if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
{
foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name)))
{
GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign);
}
}
}
public void RequestFile(FileTransferType fileType, string file, string fileHash)
{
DebugConsole.Log(

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -91,7 +91,7 @@ namespace Barotrauma.Networking
});
initializationStep = ConnectionInitialization.AuthInfoAndVersion;
timeout = NetworkConnection.TimeoutThreshold;
timeout = NetworkConnection.TimeoutThresholdNotInGame;
heartbeatTimer = 1.0;
isActive = true;
@@ -139,7 +139,7 @@ namespace Barotrauma.Networking
timeout = Screen.Selected == GameMain.GameScreen
? NetworkConnection.TimeoutThresholdInGame
: NetworkConnection.TimeoutThreshold;
: NetworkConnection.TimeoutThresholdNotInGame;
var (_, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);

View File

@@ -354,9 +354,11 @@ namespace Barotrauma
{
List<Mission> availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).Where(m => m.Prefab.ShowInMenus || GameMain.DebugDraw).ToList();
if (!availableMissions.Any()) { availableMissions.Insert(0, null); }
if (availableMissions.None()) { availableMissions.Insert(0, null); }
availableMissions.AddRange(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1]));
//show side objectives last
availableMissions.Sort((m1, m2) => (m1?.Prefab.IsSideObjective ?? false).CompareTo(m2?.Prefab.IsSideObjective ?? false));
missionList.Content.ClearChildren();
@@ -390,6 +392,10 @@ namespace Barotrauma
};
LocalizedString missionName = mission?.Name ?? TextManager.Get("NoMission");
if (mission is { Prefab.IsSideObjective: true })
{
missionName = TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), missionName);
}
if (GameMain.DebugDraw && mission != null)
{
if (!mission.Prefab.ShowInMenus) { missionName = $"[HIDDEN] {missionName}"; }
@@ -406,7 +412,7 @@ namespace Barotrauma
else
{
GUITickBox tickBox = null;
if (!isMissionInNextLocation && mission.Prefab.ShowInMenus)
if (!isMissionInNextLocation && mission.Prefab.ShowInMenus && !mission.Prefab.IsSideObjective)
{
tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionNameBlock.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionNameBlock.Padding.X, 0) }, label: string.Empty)
{
@@ -539,7 +545,7 @@ namespace Barotrauma
int missionCount = 0;
if (GameMain.GameSession != null && Campaign.Map?.CurrentLocation?.SelectedMissions != null)
{
missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m));
missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m) && !m.Prefab.IsSideObjective);
}
return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{missionCount}/{Campaign.Settings.TotalMaxMissionCount}");
}
@@ -551,9 +557,9 @@ namespace Barotrauma
OnClicked = (GUIButton btn, object obj) =>
{
if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null &&
missionList.Content.Children.Any(c => c.UserData is Mission { Prefab.ShowInMenus: true } mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation)))
missionList.Content.Children.Any(c => c.UserData is Mission { Prefab.ShowInMenus: true, Prefab.IsSideObjective: false } mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation)))
{
var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), [TextManager.Get("yes"), TextManager.Get("no")]);
noMissionVerification.Buttons[0].OnClicked = (btn, userdata) =>
{
StartRound?.Invoke();
@@ -648,7 +654,7 @@ namespace Barotrauma
private void UpdateMaxMissions(Location location)
{
hasMaxMissions = Campaign.NumberOfMissionsAtLocation(location) >= Campaign.Settings.TotalMaxMissionCount;
hasMaxMissions = Campaign.NumberOfSelectableMissionsAtLocation(location) >= Campaign.Settings.TotalMaxMissionCount;
}
public readonly struct PlayerBalanceElement

View File

@@ -5,7 +5,6 @@ using Microsoft.Xna.Framework.Graphics;
using System;
using System.Diagnostics;
using System.Linq;
using System.Transactions;
namespace Barotrauma
{
@@ -349,14 +348,25 @@ namespace Barotrauma
//------------------------------------------------------------------------
graphics.SetRenderTarget(renderTargetBackground);
if (Level.Loaded == null)
if (Level.Loaded != null)
{
graphics.Clear(new Color(11, 18, 26, 255));
Level.Loaded.DrawBack(graphics, spriteBatch, cam);
}
else if (GameMain.GameSession.GameMode is TestGameMode testMode)
{
graphics.Clear(testMode.BackgroundParams.BackgroundColor);
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap);
testMode.BackgroundParams.DrawBackgrounds(spriteBatch, cam);
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null, cam.Transform);
testMode.BackgroundParams.DrawWaterParticles(spriteBatch, cam, testMode.WaterParticleOffset);
spriteBatch.End();
}
else
{
//graphics.Clear(new Color(255, 255, 255, 255));
Level.Loaded.DrawBack(graphics, spriteBatch, cam);
graphics.Clear(new Color(11, 18, 26, 255));
}
spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform);

View File

@@ -382,6 +382,10 @@ namespace Barotrauma
GameMain.LuaCs.CheckInitialize();
}
}
else if (GameMain.Client.FileReceiver.ActiveTransfers.None())
{
GameMain.Client.RequestFile(FileTransferType.Mod, currentDownload.Name, currentDownload.Hash.StringRepresentation);
}
}
public void CurrentDownloadFinished(FileReceiver.FileTransferIn transfer)

View File

@@ -2221,11 +2221,7 @@ namespace Barotrauma
if (GameMain.Client == null) { return true; }
//the player presumably no longer wants to be afk if they clicked the start button
if (afkBox.Selected)
{
afkBox.Flash(GUIStyle.Green);
}
afkBox.Selected = false;
SetAFKSelected(false);
if (CampaignSetupFrame.Visible && CampaignSetupUI != null)
{
@@ -2357,7 +2353,7 @@ namespace Barotrauma
if (GameMain.Client != null)
{
afkBox.Visible = !GameMain.Client.IsServerOwner && GameMain.Client.ServerSettings.AllowAFK;
afkBox.Visible = GameMain.Client.IsServerOwner || GameMain.Client.ServerSettings.AllowAFK;
GameMain.Client.Voting.ResetVotes(GameMain.Client.ConnectedClients);
joinOnGoingRoundButton.OnClicked = (btn, userdata) =>
{
@@ -3082,6 +3078,15 @@ namespace Barotrauma
}
}
public void SetAFKSelected(bool selected)
{
if (afkBox.Selected != selected)
{
afkBox.Flash(GUIStyle.Green);
afkBox.Selected = selected;
}
}
public void SetAutoRestart(bool enabled, float timer = 0.0f)
{
autoRestartBox.Selected = enabled;
@@ -4994,7 +4999,7 @@ namespace Barotrauma
};
micIcon = new GUIImage(new RectTransform(new Vector2(0.05f, 1.0f), chatRow.RectTransform), style: "GUIMicrophoneUnavailable");
chatInput.Select();
chatInput.Select(ignoreSelectSound: true);
}
//this needs to be done even if we're using the existing chatinput instance instead of creating a new one,

View File

@@ -1126,6 +1126,10 @@ namespace Barotrauma
{
new GUIMessageBox(TextManager.Get("error"), TextManager.Get("CannotJoinSteamServer.SteamNotInitialized"));
}
else if (endpoint is EosP2PEndpoint && !EosInterface.Core.IsInitialized)
{
new GUIMessageBox(TextManager.Get("error"), TextManager.Get("EosStatus.NotInitialized"));
}
else
{
JoinServer(endpoint.ToEnumerable().ToImmutableArray(), "");
@@ -1183,9 +1187,29 @@ namespace Barotrauma
endpointBox.OnTextChanged += (textBox, text) =>
{
okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text);
okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text);
return true;
};
// Connect on enter press, gotta go fast
endpointBox.OnEnterPressed += (textBox, text) =>
{
if (okButton.Enabled)
{
if (okButton.PlaySoundOnSelect)
{
SoundPlayer.PlayUISound(okButton.ClickSound);
}
okButton.OnClicked.Invoke(okButton, okButton.UserData);
}
return true;
};
// Focus on and select all the text, for easier input/deletion (after the dialog is shown, apparently it takes a moment)
CoroutineManager.Invoke(() =>
{
endpointBox.Select(ignoreSelectSound: true);
}, 0.1f);
}
private void RemoveMsgFromServerList()

View File

@@ -40,6 +40,8 @@ namespace Barotrauma
#region Transform Editor
private const float TransformWidgetOffset = 300f;
private const float RotationSnapIncrement = MathF.PI / 36f;
private const float ScaleSnapIncrement = 0.1f;
private GUITickBox rotateToolToggle, scaleToolToggle;
public bool TransformWidgetSelected => TransformWidget.IsSelected;
@@ -141,6 +143,7 @@ namespace Barotrauma
if (rotateToolToggle.Selected)
{
transformCommand.RotationRad = MathUtils.VectorToAngle(PlayerInput.MousePosition - Cam.WorldToScreen(transformCommand.Pivot));
if (!PlayerInput.IsShiftDown()) { transformCommand.RotationRad = MathUtils.RoundTowardsClosest(transformCommand.RotationRad.Value, RotationSnapIncrement); }
rotationString = TextManager.GetWithVariable("SubEditor.TransformWidget.Rotation", "[value]", MathHelper.ToDegrees(transformCommand.RotationRad.Value).ToString("0.000", CultureInfo.CurrentCulture));
}
@@ -148,7 +151,9 @@ namespace Barotrauma
LocalizedString scaleString = null;
if (scaleToolToggle.Selected)
{
transformCommand.ScaleMult = Math.Clamp(Vector2.Distance(PlayerInput.MousePosition, Cam.WorldToScreen(transformCommand.Pivot)) / (TransformWidgetOffset * GUI.Scale), transformCommand.MinScale, transformCommand.MaxScale);
transformCommand.ScaleMult = Vector2.Distance(PlayerInput.MousePosition, Cam.WorldToScreen(transformCommand.Pivot)) / (TransformWidgetOffset * GUI.Scale);
if (!PlayerInput.IsShiftDown()) { transformCommand.ScaleMult = MathUtils.RoundTowardsClosest(transformCommand.ScaleMult.Value, ScaleSnapIncrement); }
transformCommand.ScaleMult = Math.Clamp(transformCommand.ScaleMult.Value, transformCommand.MinScale, transformCommand.MaxScale);
scaleString = TextManager.GetWithVariable("SubEditor.TransformWidget.Scale", "[value]", transformCommand.ScaleMult.Value.ToString("0.000", CultureInfo.CurrentCulture));
}

View File

@@ -260,8 +260,16 @@ namespace Barotrauma
{
continue;
}
split[j] = split[j].Replace(" & ", " &amp; ");
xmlContentByLanguage[languageName].Add($"<{split[0]}>{split[j]}</{split[0]}>");
string textContent = split[j];
if (textContent == "#NAME?")
{
throw new Exception(
$"Error while converting csv to xml: #NAME? value found on line {row}, language: {languageName}." +
" This indicates a missing value in the csv file (some text got accidentally converted to a broken formula in the localization sheet?).");
}
textContent = textContent.Replace(" & ", " &amp; ");
xmlContentByLanguage[languageName].Add($"<{split[0]}>{textContent}</{split[0]}>");
}
}
}

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.10.7.2</Version>
<Version>1.11.4.1</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.10.7.2</Version>
<Version>1.11.4.1</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.10.7.2</Version>
<Version>1.11.4.1</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.10.7.2</Version>
<Version>1.11.4.1</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.10.7.2</Version>
<Version>1.11.4.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -53,7 +53,7 @@ namespace Barotrauma
{
if (!Enabled) { return 1000.0f; }
Vector2 comparePosition = recipient.SpectatePos == null ? recipient.Character.WorldPosition : recipient.SpectatePos.Value;
Vector2 comparePosition = recipient.SpectatePos ?? recipient.Character.WorldPosition;
float distance = Vector2.Distance(comparePosition, WorldPosition);
if (recipient.Character?.ViewTarget != null)
@@ -199,7 +199,9 @@ namespace Barotrauma
UInt16 networkUpdateID = msg.ReadUInt16();
byte inputCount = msg.ReadByte();
if (AllowInput) { Enabled = true; }
// Doesn't seem to work consistently (at least with simulated long loading time 120), because sometimes there's some stun on the character. Anyway, can't see why we'd have to check AllowInput here.
//if (AllowInput) { Enabled = true; }
Enabled = true;
for (int i = 0; i < inputCount; i++)
{
@@ -802,9 +804,7 @@ namespace Barotrauma
if (msg.LengthBytes - initialMsgLength >= 255 && restrictMessageSize)
{
string errorMsg = $"Error when writing character spawn data for \"{Name}\": data exceeded 255 bytes (info: {infoLength}, orders: {ordersLength}, total: {msg.LengthBytes - initialMsgLength})";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchData", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.AddWarning($"Character spawn data for \"{Name}\" exceeded 255 bytes (info: {infoLength}, orders: {ordersLength}, total: {msg.LengthBytes - initialMsgLength})");
}
TryWriteStatus(msg);
@@ -820,9 +820,7 @@ namespace Barotrauma
msg.WriteBoolean(false);
if (msgLengthBeforeStatus < 255)
{
string errorMsg = $"Error when writing character spawn data for \"{Name}\": status data caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchDataForStatus", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError($"Character spawn data for \"{Name}\" caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})");
}
}
else

View File

@@ -278,7 +278,14 @@ namespace Barotrauma
characterInfo.ApplyDeathEffects();
}
c.CharacterInfo = characterInfo;
SetClientCharacterData(c);
// Only create new character data if the connected client has an active character (which they might not,
// eg. if they are in the lobby). Otherwise the CharacterCampaignData constructor would fall back to a new
// Character object, overwriting the inventory and wallet with empty values.
if (c.Character != null)
{
SetClientCharacterData(c);
}
}
//refresh the character data of clients who aren't in the server anymore
@@ -1105,10 +1112,6 @@ namespace Barotrauma
bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character);
}
}
else
{
GameServer.Log($"{sender.Name} attempted to buy or sell items without having access to a store NPC.", ServerLog.MessageType.Error);
}
if ((purchasedUpgrades.Any() || purchasedItemSwaps.Any()) &&
HasCampaignInteractionAvailable(sender, InteractionType.Upgrade))
@@ -1142,10 +1145,6 @@ namespace Barotrauma
}
}
}
else
{
GameServer.Log($"{sender.Name} attempted to buy upgrades without having access to an NPC offering upgrades.", ServerLog.MessageType.Error);
}
}
private bool HasCampaignInteractionAvailable(Client sender, InteractionType interactionType)

View File

@@ -0,0 +1,48 @@
using Barotrauma.Networking;
using System.Collections.Generic;
namespace Barotrauma.Items.Components
{
partial class ConnectionSelectorComponent : ItemComponent
{
private CoroutineHandle sendStateCoroutine;
private int lastSentConnectionIndex;
private float sendStateTimer;
partial void OnStateChanged()
{
sendStateTimer = 0.5f;
if (sendStateCoroutine == null)
{
sendStateCoroutine = CoroutineManager.StartCoroutine(SendStateAfterDelay());
}
}
private IEnumerable<CoroutineStatus> SendStateAfterDelay()
{
while (sendStateTimer > 0.0f)
{
sendStateTimer -= CoroutineManager.DeltaTime;
yield return CoroutineStatus.Running;
}
if (item.Removed || GameMain.NetworkMember == null)
{
yield return CoroutineStatus.Success;
}
sendStateCoroutine = null;
if (lastSentConnectionIndex != selectedConnectionIndex)
{
item.CreateServerEvent(this);
}
yield return CoroutineStatus.Success;
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.WriteRangedInteger(selectedConnectionIndex, 0, 255);
lastSentConnectionIndex = selectedConnectionIndex;
}
}
}

View File

@@ -1857,6 +1857,9 @@ namespace Barotrauma.Networking
}
else
{
//client presumably isn't afk if they clicked to start a round
sender.AFK = false;
bool continueCampaign = inc.ReadBoolean();
if (mpCampaign != null && mpCampaign.GameOver || continueCampaign)
{
@@ -2042,7 +2045,8 @@ namespace Barotrauma.Networking
}
}
if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave))
//don't send the campaign save if there's any other transfers running (client waiting for subs, mods, or already transferring the campaign save)
if (FileSender.ActiveTransfers.None(t => t.Connection == c.Connection))
{
FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.DataPath.SavePath);
c.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)NetTime.Now);
@@ -3077,7 +3081,7 @@ namespace Barotrauma.Networking
WayPoint jobItemSpawnPoint = mainSubWaypoints != null ? mainSubWaypoints[i] : spawnWaypoints[i];
Character spawnedCharacter = Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, isRemotePlayer: true, hasAi: false);
spawnedCharacter.AnimController.Frozen = true;
//spawnedCharacter.AnimController.Frozen = true;
spawnedCharacter.TeamID = teamID;
teamClients[i].Character = spawnedCharacter;
var characterData = campaign?.GetClientCharacterData(teamClients[i]);
@@ -4359,7 +4363,7 @@ namespace Barotrauma.Networking
public void SetClientCharacter(Client client, Character newCharacter)
{
if (client == null) return;
if (client == null) { return; }
//the client's previous character is no longer a remote player
if (client.Character != null)
@@ -4385,13 +4389,14 @@ namespace Barotrauma.Networking
newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID;
}
if (newCharacter.Info != null && newCharacter.Info.Character == null)
if (newCharacter.Info is { Character: null })
{
newCharacter.Info.Character = newCharacter;
}
newCharacter.SetOwnerClient(client);
newCharacter.Enabled = true;
newCharacter.AnimController.Frozen = false;
client.Character = newCharacter;
client.CharacterInfo = newCharacter.Info;
CreateEntityEvent(newCharacter, new Character.ControlEventData(client));

View File

@@ -128,11 +128,11 @@ namespace Barotrauma.Networking
//remove old events that have been sent to all clients, they are redundant now
// keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
// and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced
if (GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration)
if (GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration)
{
events.RemoveAll(e =>
(NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) &&
e.CreateTime < Timing.TotalTime - NetConfig.EventRemovalTime);
e.CreateTime < Timing.TotalTime - server.ServerSettings.EventRemovalTime);
}
for (int i = events.Count - 1; i >= 0; i--)
@@ -226,7 +226,7 @@ namespace Barotrauma.Networking
if (Timing.TotalTime - lastWarningTime > 5.0 &&
Timing.TotalTime - lastSentToAnyoneTime > 10.0 &&
GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration)
GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration)
{
lastWarningTime = Timing.TotalTime;
string warningMsg = $"WARNING: ServerEntityEventManager is lagging behind! Last sent id: {lastSentToAnyone}, latest create id: {ID}";
@@ -240,8 +240,8 @@ namespace Barotrauma.Networking
ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
if (firstEventToResend != null &&
GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration &&
((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime))
GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration &&
((lastSentToAnyoneTime - firstEventToResend.CreateTime) > server.ServerSettings.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > server.ServerSettings.OldEventKickTime))
{
// This event is 10 seconds older than the last one we've successfully sent,
// kick everyone that hasn't received it yet, this is way too old

View File

@@ -55,13 +55,13 @@ namespace Barotrauma.Networking
PasswordRetries = 0;
PasswordSalt = null;
UpdateTime = Timing.TotalTime + Timing.Step * 3.0;
TimeOut = NetworkConnection.TimeoutThreshold;
TimeOut = NetworkConnection.TimeoutThresholdNotInGame;
AuthSessionStarted = false;
}
public void Heartbeat()
{
TimeOut = NetworkConnection.TimeoutThreshold;
TimeOut = NetworkConnection.TimeoutThresholdNotInGame;
}
}
@@ -124,7 +124,7 @@ namespace Barotrauma.Networking
protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep)
{
pendingClient.TimeOut = NetworkConnection.TimeoutThreshold;
pendingClient.TimeOut = NetworkConnection.TimeoutThresholdNotInGame;
if (pendingClient.InitializationStep != initializationStep) { return; }

View File

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

View File

@@ -0,0 +1,6 @@
<Missions>
<Mission identifier="missionvariant" variantof="killcrawlerswarm1" name="One Million Crawlers" description=":3" difficulty="4" commonness="100000000" reward="20000" sonarlabel="character.crawler">
<monster min="25" max="50"/>
<Reputation amount="10"/>
</Mission>
</Missions>

View File

@@ -0,0 +1,3 @@
<contentpackage name="Mission Variants Test" modversion="1.0.0" corepackage="False" gameversion="1.10.2.0">
<Missions file="%ModDir%/Missions.xml"/>
</contentpackage>

View File

@@ -1223,6 +1223,7 @@ namespace Barotrauma
if (otherCharacter.SelectedCharacter == null ||
!otherCharacter.SelectedCharacter.IsDead ||
otherCharacter.SelectedCharacter.TeamID != Character.TeamID ||
otherCharacter.IsPet ||
otherCharacter.IsInstigator)
{
continue;

View File

@@ -114,7 +114,7 @@ namespace Barotrauma
if (target.Submarine != character.Submarine) { return; }
Reset();
TargetCharacter = target;
targetBody = target.AnimController.Collider.FarseerBody;
targetBody = target.AnimController.MainLimb.body.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}

View File

@@ -769,7 +769,7 @@ namespace Barotrauma
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
lethalDmg = attack.GetTotalCharacterDamage();
float max = lethalDmg + 1;
if (weapon.Item.HasTag(Tags.StunnerItem))
{
@@ -795,7 +795,7 @@ namespace Barotrauma
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
lethalDmg = attack.GetTotalCharacterDamage();
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
if (diff < 0)
@@ -809,7 +809,7 @@ namespace Barotrauma
{
// Cannot do stun damage -> use the melee damage to determine the priority.
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
priority = attack?.GetTotalCharacterDamage() ?? priority / 2;
}
// Reduce the priority of the weapon, if we don't have requires skills to use it.
float startPriority = priority;
@@ -960,7 +960,7 @@ namespace Barotrauma
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
lethalDmg = attack.GetTotalCharacterDamage();
}
return lethalDmg;
}

View File

@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
@@ -94,6 +95,7 @@ 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; }
float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f);
if (distFactor > bestDistFactor)
{

View File

@@ -1,6 +1,7 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -30,6 +31,7 @@ namespace Barotrauma
if (character.Submarine == null ||
Item.ItemList.None(it =>
it.GetComponent<Deconstructor>() != null &&
!it.IgnoreByAI(character) &&
it.IsInteractable(character) &&
character.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true, allowDifferentTeam: true, allowDifferentType: true)))
{
@@ -60,6 +62,9 @@ namespace Barotrauma
protected override bool IsValidTarget(Item target)
{
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 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

@@ -98,18 +98,11 @@ namespace Barotrauma
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
if (affliction.Strength > affliction.Prefab.TreatmentThreshold)
{
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
//vitality loss is not required to treat this affliction -> evaluate the strength of the affliction too
if (!affliction.Prefab.VitalityLossRequiredForTreatment)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab == AfflictionPrefab.HuskInfection)
{
vitality -= affliction.Strength;
}
}
}
return Math.Clamp(vitality, 0, 100);

View File

@@ -437,8 +437,25 @@ namespace Barotrauma
public bool TargetItemsMatchItem(Item item, Identifier option = default)
{
if (item == null) { return false; }
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct && !Item.DeconstructItems.Contains(item)) { return true; }
if (Identifier == Tags.DontDeconstructThis && Item.DeconstructItems.Contains(item)) { return true; }
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct)
{
if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) &&
//only allow deconstructing if there are deconstruction recipes that
item.Prefab.DeconstructItems.Any(deconstructItem =>
//1. don't require any additional items (bots can't handle that)
deconstructItem.RequiredOtherItem.None() &&
//2. don't require a research station (bots don't know how to use those)
(deconstructItem.RequiredDeconstructor.Length == 0 || deconstructItem.RequiredDeconstructor.Any(d => d != Tags.GeneticResearchStation))))
{
return true;
}
}
else if (Identifier == Tags.DontDeconstructThis)
{
if (Item.DeconstructItems.Contains(item)) { return true; }
}
ImmutableArray<Identifier> targetItems = GetTargetItems(option);
return TargetItemsMatchItem(targetItems, item);
}

View File

@@ -216,6 +216,7 @@ namespace Barotrauma
{
UpdateTemporaryAnimations();
UpdateAnim(deltaTime);
CheckRopeState();
}
protected abstract void UpdateAnim(float deltaTime);
@@ -1134,6 +1135,25 @@ namespace Barotrauma
character.TeleportTo(pos);
}
protected void CheckRopeState()
{
if (!shouldHangWithRope)
{
StopHangingWithRope();
}
if (!shouldHoldToRope)
{
StopHoldingToRope();
}
if (!shouldBeDraggedWithRope)
{
StopGettingDraggedWithRope();
}
shouldHoldToRope = false;
shouldHangWithRope = false;
shouldBeDraggedWithRope = false;
}
private void StartAnimation(Animation animation)
{
if (animation == Animation.UsingItem)

View File

@@ -478,21 +478,6 @@ namespace Barotrauma
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
if (!shouldHangWithRope)
{
StopHangingWithRope();
}
if (!shouldHoldToRope)
{
StopHoldingToRope();
}
if (!shouldBeDraggedWithRope)
{
StopGettingDraggedWithRope();
}
shouldHoldToRope = false;
shouldHangWithRope = false;
shouldBeDraggedWithRope = false;
}
void UpdateStanding()
@@ -1256,34 +1241,25 @@ namespace Barotrauma
float prevVitality = target.Vitality;
bool wasCritical = prevVitality < 0.0f;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
float skill = character.GetSkillLevel(Tags.MedicalSkill);
bool oxygenAvailable = target.OxygenAvailable >= CharacterHealth.InsufficientOxygenThreshold;
//Serverside code
if (oxygenAvailable && GameMain.NetworkMember is not { IsClient: true })
{
target.Oxygen += deltaTime * 0.5f; //Stabilize them
}
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel(Tags.MedicalSkill);
if (GameMain.NetworkMember is not { IsClient: true })
{
if (cprBoost >= 1f)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
}
}
//Serverside code
if (GameMain.NetworkMember is not { IsClient: true })
{
if (target.Oxygen < -10.0f)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
target.Oxygen += stabilizationAmount * deltaTime;
}
}
@@ -1317,7 +1293,7 @@ namespace Barotrauma
}
//need to CPR for at least a couple of seconds before the target can be revived
//(reviving the target when the CPR has barely started looks strange)
if (cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true })
if (oxygenAvailable && cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true })
{
float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
@@ -1343,7 +1319,7 @@ namespace Barotrauma
//got the character back into a non-critical state, increase medical skill
//BUT only if it has been more than 10 seconds since the character revived someone
//otherwise it's easy to abuse the system by repeatedly reviving in a low-oxygen room
if (!target.IsDead)
if (!target.IsDead || !oxygenAvailable)
{
target.CharacterHealth.RecalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)

View File

@@ -76,7 +76,7 @@ namespace Barotrauma
get { return frozen; }
set
{
if (frozen == value) return;
if (frozen == value) { return; }
frozen = value;
@@ -1063,7 +1063,8 @@ namespace Barotrauma
}
}
public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true)
/// <param name="setInWater">Should the character be immediately considered "in water" if it's outside hulls (normally checked in Update)</param>
public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true, bool setInWater = false)
{
Vector2 findPos = worldPosition == null ? this.WorldPosition : (Vector2)worldPosition;
if (!MathUtils.IsValid(findPos))
@@ -1076,6 +1077,10 @@ namespace Barotrauma
}
Hull newHull = Hull.FindHull(findPos, currentHull);
if (setInWater && newHull == null)
{
inWater = true;
}
if (newHull == currentHull) { return; }

View File

@@ -403,14 +403,19 @@ namespace Barotrauma
return (Duration == 0.0f) ? dmg : dmg * deltaTime;
}
public float GetTotalDamage(bool includeStructureDamage = false)
/// <summary>
/// Returns the total damage (vitality decrease) this attack causes on characters.
/// </summary>
public float GetTotalCharacterDamage()
{
float totalDamage = includeStructureDamage ? StructureDamage : 0.0f;
float totalDamage = 0.0f;
foreach (Affliction affliction in Afflictions.Keys)
{
totalDamage += affliction.GetVitalityDecrease(null);
float afflictionVitalityDecrease = affliction.GetVitalityDecrease(null);
if (affliction.AffectedByAttackMultipliers) { afflictionVitalityDecrease *= DamageMultiplier; }
totalDamage += afflictionVitalityDecrease;
}
return totalDamage * DamageMultiplier;
return totalDamage;
}
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)

View File

@@ -30,7 +30,7 @@ namespace Barotrauma
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{
public readonly static List<Character> CharacterList = new List<Character>();
public static readonly List<Character> CharacterList = new List<Character>();
public static int CharacterUpdateInterval = 1;
private static int characterUpdateTick = 1;
@@ -42,7 +42,13 @@ namespace Barotrauma
partial void UpdateLimbLightSource(Limb limb);
private bool enabled = true;
private bool initialized;
private bool enabled;
//characters start disabled in the multiplayer mode, and are enabled if/when
// - controlled by the player
// - client receives a position update from the server
// - server receives an input message from the client controlling the character
// - if an AICharacter, the server enables it when close enough to any of the players
public bool Enabled
{
get
@@ -51,7 +57,12 @@ namespace Barotrauma
}
set
{
if (value == enabled) { return; }
if (initialized && value == enabled)
{
// Ensure that we'll set the value and run the code below at least once, because otherwise the states might be out of sync.
return;
}
initialized = true;
if (Removed)
{
@@ -83,7 +94,6 @@ namespace Barotrauma
//we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs
item.body.Enabled = true;
}
}
AnimController.Collider.Enabled = value;
}
@@ -112,6 +122,13 @@ namespace Barotrauma
if (!CharacterList.Contains(this)) { CharacterList.Add(this); }
if (AiTarget != null && !AITarget.List.Contains(AiTarget)) { AITarget.List.Add(AiTarget); }
}
if (Inventory != null)
{
foreach (var item in Inventory.FindAllItems(recursive: true))
{
item.IsActive = !disabledByEvent;
}
}
}
}
@@ -1625,18 +1642,14 @@ namespace Barotrauma
PressureProtection = int.MaxValue;
}
AnimController.SetPosition(ConvertUnits.ToSimUnits(position));
CharacterHealth.CheckForErrors();
AnimController.FindHull(null);
AnimController.SetPosition(ConvertUnits.ToSimUnits(position));
AnimController.FindHull(setInWater: true);
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
CharacterList.Add(this);
//characters start disabled in the multiplayer mode, and are enabled if/when
// - controlled by the player
// - client receives a position update from the server
// - server receives an input message from the client controlling the character
// - if an AICharacter, the server enables it when close enough to any of the players
Enabled = GameMain.NetworkMember == null;
if (info != null)
@@ -3311,17 +3324,22 @@ namespace Barotrauma
public static void UpdateAll(float deltaTime, Camera cam)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) // single player or server
{
foreach (Character c in CharacterList)
{
if (c is not AICharacter && !c.IsRemotePlayer) { continue; }
if (c.IsPlayer || (c.IsBot && !c.IsDead))
// TODO: The logic below seems to be overly complicated and quite confusing
if (c is not AICharacter && !c.IsRemotePlayer) { continue; } // confusing -> what this line is intended for? local player? But that's handled below...
if (c.IsRemotePlayer)
{
// Let the client tell when to enable the character. If we force it enabled here, it may e.g. get killed while still loading a round.
continue;
}
if (c.IsLocalPlayer || (c.IsBot && !c.IsDead))
{
c.Enabled = true;
}
else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) // mp server
{
//disable AI characters that are far away from all clients and the host's character and not controlled by anyone
float closestPlayerDist = c.GetDistanceToClosestPlayer();
@@ -3338,7 +3356,7 @@ namespace Barotrauma
c.Enabled = true;
}
}
else if (Submarine.MainSub != null)
else if (Submarine.MainSub != null) // sp only?
{
//disable AI characters that are far away from the sub and the controlled character
float distSqr = Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition);
@@ -3382,7 +3400,7 @@ namespace Barotrauma
foreach (Character character in GameMain.LuaCs.Game.UpdatePriorityCharacters)
{
if (character.Removed) { continue; }
Debug.Assert(character is { Removed: false });
character.Update(deltaTime, cam);
}
@@ -3444,8 +3462,7 @@ namespace Barotrauma
foreach (Item item in Inventory.GetAllItems(checkForDuplicates: false))
{
if (item.body == null || item.body.Enabled) { continue; }
item.SetTransform(SimPosition, 0.0f);
item.Submarine = Submarine;
item.SetTransform(SimPosition, 0.0f, forceSubmarine: Submarine);
}
}
@@ -4600,7 +4617,10 @@ namespace Barotrauma
SetStun(stun);
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
if (attacker != null && attacker != this &&
attacker.IsOnPlayerTeam &&
GameMain.NetworkMember != null &&
!GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
if (attacker.TeamID == TeamID)
{

View File

@@ -85,6 +85,8 @@ namespace Barotrauma
public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime;
public bool AffectedByAttackMultipliers => Prefab.AffectedByAttackMultipliers;
public float Duration;
/// <summary>

View File

@@ -164,15 +164,18 @@ namespace Barotrauma
}
break;
case InfectionState.Transition:
if (character == Character.Controlled)
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
{
if (character == Character.Controlled)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red);
GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red);
#endif
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier());
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier());
}
}
break;
case InfectionState.Active:

View File

@@ -699,7 +699,13 @@ namespace Barotrauma
/// and the health UI will render the affected limb in green rather than red.
/// </summary>
public readonly bool IsBuff;
/// <summary>
/// Should the affliction be affected by damage multipliers on an attack (e.g. when the attacker has talents that boost damage).
/// By default, afflictions defined as buffs aren't affected.
/// </summary>
public readonly bool AffectedByAttackMultipliers;
/// <summary>
/// If set to true, this affliction can affect characters that are marked as
/// machines, such as the Fractal Guardian.
@@ -780,6 +786,14 @@ namespace Barotrauma
/// </summary>
public readonly float TreatmentSuggestionThreshold;
/// <summary>
/// Does the affliction need to have caused some amount of vitality loss for bots to consider treating it?
/// Normally bots use vitality loss as a way to determine what kind of injuries need treatment, but some afflictions (e.g. poisons, infections)
/// might require treatment regardless of the vitality loss. If disabled, the bots will use the strength of the affliction to evaluate the severity instead of the vitality loss.
/// Defaults to true for all afflictions that aren't of the type Paralysis, Poison or HuskInfection.
/// </summary>
public readonly bool VitalityLossRequiredForTreatment;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
/// </summary>
@@ -838,14 +852,15 @@ namespace Barotrauma
public readonly bool DamageParticles;
/// <summary>
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
/// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile.
/// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly.
/// A modifier that affects how much medical skill is increased when you apply this affliction on a target.
/// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile, and the modifier is multiplied by the amount of vitality the enemy lost.
/// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly, and the modifier is multiplied by the strength of the affliction the target gained.
/// </summary>
public readonly float MedicalSkillGain;
/// <summary>
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// A modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// Multiplied by the amount of vitality the enemy lost.
/// The skill is increased only when the target is hostile.
/// </summary>
public readonly float WeaponsSkillGain;
@@ -925,6 +940,7 @@ namespace Barotrauma
ShowDescriptionInTooltip = element.GetAttributeBool(nameof(ShowDescriptionInTooltip), true);
IsBuff = element.GetAttributeBool(nameof(IsBuff), false);
AffectedByAttackMultipliers = element.GetAttributeBool(nameof(AffectedByAttackMultipliers), def: !IsBuff);
AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true);
ShowBarInHealthMenu = element.GetAttributeBool("showbarinhealthmenu", true);
@@ -977,6 +993,11 @@ namespace Barotrauma
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
TreatmentSuggestionThreshold = element.GetAttributeFloat(nameof(TreatmentSuggestionThreshold), TreatmentThreshold);
bool alwaysRequiresTreatment = AfflictionType == ParalysisType || AfflictionType == PoisonType || this is AfflictionPrefabHusk;
VitalityLossRequiredForTreatment = element.GetAttributeBool(nameof(VitalityLossRequiredForTreatment),
def: !alwaysRequiresTreatment);
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);

View File

@@ -309,6 +309,19 @@ namespace Barotrauma
InitProjSpecific(element, character);
}
public void CheckForErrors()
{
for (int i = 0; i < limbHealths.Count; i++)
{
if (Character.AnimController.Limbs.None(l => l.HealthIndex == i))
{
DebugConsole.AddWarning(
$"Potential error in character {Character.DisplayName}: none of the limbs have been set to use the LimbHealth #{i}, and it will do nothing. "
+ "Did you forget to set the HealthIndex values of the limbs?", contentPackage: Character.ContentPackage);
}
}
}
private void InitIrremovableAfflictions()
{
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
@@ -1132,20 +1145,38 @@ namespace Barotrauma
// We need to use another list of the afflictions when we call the status effects triggered by afflictions,
// because those status effects may add or remove other afflictions while iterating the collection.
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
private readonly List<Affliction> afflictionsCopy = [];
private bool isApplyingAfflictionStatusEffects;
public void ApplyAfflictionStatusEffects(ActionType type)
{
afflictionsCopy.Clear();
afflictionsCopy.AddRange(afflictions.Keys);
foreach (Affliction affliction in afflictionsCopy)
if (isApplyingAfflictionStatusEffects)
{
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
//pretty hacky: if we're already in the process of applying afflictions' status effects
//(i.e. calling this method caused some additional afflictions to appear and trigger status effects)
//let's instantiate a new list so we don't end up modifying afflictionsCopy while enumerating it
foreach (Affliction affliction in afflictions.Keys.ToList())
{
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
}
}
else
{
isApplyingAfflictionStatusEffects = true;
afflictionsCopy.Clear();
afflictionsCopy.AddRange(afflictions.Keys);
isApplyingAfflictionStatusEffects = true;
foreach (Affliction affliction in afflictionsCopy)
{
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
}
isApplyingAfflictionStatusEffects = false;
}
}
public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath()
{
List<Affliction> currentAfflictions = GetAllAfflictions(true);
IEnumerable<Affliction> currentAfflictions = GetAllAfflictions(true);
Affliction strongestAffliction = null;
float largestStrength = 0.0f;
@@ -1168,7 +1199,7 @@ namespace Barotrauma
}
private readonly List<Affliction> allAfflictions = new List<Affliction>();
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{
allAfflictions.Clear();
if (!mergeSameAfflictions)

View File

@@ -843,7 +843,7 @@ namespace Barotrauma
}
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier;
float finalDamageModifier = affliction.AffectedByAttackMultipliers ? damageMultiplier : 1.0f;
if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType)
{
finalDamageModifier *= character.EmpVulnerability;

View File

@@ -14,14 +14,41 @@ namespace Barotrauma
public enum SpawnLocationType
{
Any,
/// <summary>
/// Spawnpoint inside the main submarine.
/// </summary>
MainSub,
/// <summary>
/// Spawnpoint inside an outpost.
/// </summary>
Outpost,
/// <summary>
/// Spawnpoint on the main path through the level.
/// </summary>
MainPath,
/// <summary>
/// Spawnpoint in a cave. Only valid if there are caves in the level.
/// </summary>
Cave,
/// <summary>
/// Spawnpoint in an abyss cave. Only valid if there are abyss caves in the level.
/// </summary>
AbyssCave,
/// <summary>
/// Spawnpoint in a ruin. Only valid if there are ruins in the level.
/// </summary>
Ruin,
/// <summary>
/// Spawnpoint in a wreck. Only valid if there are wrecks in the level.
/// </summary>
Wreck,
/// <summary>
/// Spawnpoint in a beacon station. Only valid if there are beacon stations in the level.
/// </summary>
BeaconStation,
/// <summary>
/// A spawnpoint on the main path through the level. The difference to the <see cref="MainPath"/> type is that the closest possible spawnpoint is chosen.
/// </summary>
NearMainSub
}
@@ -425,7 +452,7 @@ namespace Barotrauma
}
else
{
spawnPointsWithCorrectType = potentialSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path);
spawnPointsWithCorrectType = potentialSpawnPoints;
}
if (spawnPointsWithCorrectType.Any())
{
@@ -485,7 +512,12 @@ namespace Barotrauma
}
//spawnpoints that match the desired criteria found, choose the best one next
IEnumerable<WayPoint> validSpawnPoints = potentialSpawnPoints;
// preferring non-path spawnpoints if there's any available
var nonPathSpawnPoints = potentialSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path);
var validSpawnPoints =
nonPathSpawnPoints.Any() && spawnPointType != SpawnType.Path ?
nonPathSpawnPoints :
potentialSpawnPoints;
//don't spawn in an airlock module if there are other options
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false);

View File

@@ -8,9 +8,9 @@ using System.Xml.Linq;
namespace Barotrauma
{
partial class MissionPrefab : PrefabWithUintIdentifier
internal sealed partial class MissionPrefab : PrefabWithUintIdentifier, IImplementsVariants<MissionPrefab>
{
public static readonly PrefabCollection<MissionPrefab> Prefabs = new PrefabCollection<MissionPrefab>();
public static readonly PrefabCollection<MissionPrefab> Prefabs = [];
/// <summary>
/// The keys here are for backwards compatibility, tying the old mission types to the appropriate class.
@@ -42,7 +42,7 @@ namespace Barotrauma
{ "Combat".ToIdentifier(), typeof(CombatMission) }
};
public static readonly HashSet<Identifier> HiddenMissionTypes = new HashSet<Identifier>() { "GoTo".ToIdentifier(), "End".ToIdentifier() };
public static readonly HashSet<Identifier> HiddenMissionTypes = ["GoTo".ToIdentifier(), "End".ToIdentifier()];
public class ReputationReward
{
@@ -58,110 +58,117 @@ namespace Barotrauma
}
}
private readonly ConstructorInfo constructor;
private ConstructorInfo constructor;
public readonly Identifier Type;
public Identifier Type { get; private set; }
public readonly Type MissionClass;
public Type MissionClass { get; private set; }
public readonly bool MultiplayerOnly, SingleplayerOnly;
public bool MultiplayerOnly { get; private set; }
public bool SingleplayerOnly { get; private set; }
public readonly Identifier TextIdentifier;
public Identifier TextIdentifier { get; private set; }
public readonly ImmutableHashSet<Identifier> Tags;
public ImmutableHashSet<Identifier> Tags { get; private set; }
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly LocalizedString SuccessMessage;
public readonly LocalizedString FailureMessage;
public readonly LocalizedString SonarLabel;
public readonly Identifier SonarIconIdentifier;
public LocalizedString Name { get; private set; }
public LocalizedString Description { get; private set; }
public LocalizedString SuccessMessage { get; private set; }
public LocalizedString FailureMessage { get; private set; }
public LocalizedString SonarLabel { get; private set; }
public Identifier SonarIconIdentifier { get; private set; }
public readonly Identifier AchievementIdentifier;
public Identifier AchievementIdentifier { get; private set; }
public readonly ImmutableList<ReputationReward> ReputationRewards;
public ImmutableList<ReputationReward> ReputationRewards { get; private set; }
public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>
DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>();
public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> DataRewards = [];
public readonly int Commonness;
public int Commonness { get; private set; }
/// <summary>
/// Displayed difficulty (indicator)
/// </summary>
public readonly int? Difficulty;
public int? Difficulty { get; private set; }
public const int MinDifficulty = 1, MaxDifficulty = 4;
/// <summary>
/// The actual minimum difficulty of the level allowed for this mission to trigger.
/// </summary>
public readonly int MinLevelDifficulty = 0;
public int MinLevelDifficulty { get; private set; } = 0;
/// <summary>
/// The actual maximum difficulty of the level allowed for this mission to trigger.
/// </summary>
public readonly int MaxLevelDifficulty = 100;
public int MaxLevelDifficulty { get; private set; } = 100;
public readonly int Reward;
public int Reward { get; private set; }
public readonly float ExperienceMultiplier;
public float ExperienceMultiplier { get; private set; }
// The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters.
public readonly ImmutableArray<LocalizedString> Headers;
public readonly ImmutableArray<LocalizedString> Messages;
public ImmutableArray<LocalizedString> Headers { get; private set; }
public ImmutableArray<LocalizedString> Messages { get; private set; }
public readonly bool AllowRetry;
public bool AllowRetry { get; private set; }
public readonly bool ShowSonarLabels;
public bool ShowSonarLabels { get; private set; }
public readonly bool ShowInMenus, ShowStartMessage;
public bool ShowInMenus { get; private set; }
public bool ShowStartMessage { get; private set; }
public readonly bool IsSideObjective;
/// <summary>
/// Makes the mission not count for the maximum mission limit, and forces it to always be selected when it's available in a level.
/// </summary>
public bool IsSideObjective { get; private set; }
public readonly bool AllowOtherMissionsInLevel;
public bool AllowOtherMissionsInLevel { get; private set; }
public readonly bool RequireWreck, RequireRuin, RequireBeaconStation, RequireThalamusWreck;
public readonly bool SpawnBeaconStationInMiddle;
public bool RequireWreck { get; private set; }
public bool RequireRuin { get; private set; }
public bool RequireBeaconStation { get; private set; }
public bool RequireThalamusWreck { get; private set; }
public bool SpawnBeaconStationInMiddle { get; private set; }
public readonly bool AllowOutpostNPCs;
public bool AllowOutpostNPCs { get; private set; }
public readonly Identifier ForceOutpostGenerationParameters;
public Identifier ForceOutpostGenerationParameters { get; private set; }
public readonly RespawnMode? ForceRespawnMode;
public RespawnMode? ForceRespawnMode { get; private set; }
/// <summary>
/// If set, the players can choose which outpost is used for the mission (selected from the outposts that have this tag). Only works in multiplayer.
/// </summary>
public readonly Identifier AllowOutpostSelectionFromTag;
public Identifier AllowOutpostSelectionFromTag { get; private set; }
public readonly bool LoadSubmarines = true;
public bool LoadSubmarines { get; private set; } = true;
/// <summary>
/// If enabled, locations this mission takes place in cannot change their type
/// </summary>
public readonly bool BlockLocationTypeChanges;
public bool BlockLocationTypeChanges { get; private set; }
public readonly bool ShowProgressBar;
public readonly bool ShowProgressInNumbers;
public readonly int MaxProgressState;
public readonly LocalizedString ProgressBarLabel;
public bool ShowProgressBar { get; private set; }
public bool ShowProgressInNumbers { get; private set; }
public int MaxProgressState { get; private set; }
public LocalizedString ProgressBarLabel { get; private set; }
/// <summary>
/// The mission can only be received when travelling from a location of the first type to a location of the second type
/// </summary>
public readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes;
public List<(Identifier from, Identifier to)> AllowedConnectionTypes { get; private set; }
/// <summary>
/// The mission can only be received in these location types
/// </summary>
public readonly List<Identifier> AllowedLocationTypes = new List<Identifier>();
public readonly List<Identifier> AllowedLocationTypes = [];
/// <summary>
/// The mission can only happen in locations owned by this faction. In the mission mode, the location is forced to be owned by this faction.
/// </summary>
public readonly Identifier RequiredLocationFaction;
public Identifier RequiredLocationFaction { get; private set; }
/// <summary>
/// Show entities belonging to these sub categories when the mission starts
/// </summary>
public readonly List<string> UnhideEntitySubCategories = new List<string>();
public List<string> UnhideEntitySubCategories { get; private set; }
public class TriggerEvent
{
@@ -186,22 +193,39 @@ namespace Barotrauma
}
}
public readonly List<TriggerEvent> TriggerEvents = new List<TriggerEvent>();
public readonly List<TriggerEvent> TriggerEvents = [];
public LocationTypeChange LocationTypeChangeOnCompleted;
public readonly ContentXElement ConfigElement;
private readonly ContentXElement originalElement;
public ContentXElement ConfigElement { get; private set; }
public Identifier VariantOf { get; }
public MissionPrefab ParentPrefab { get; set; }
public MissionPrefab(ContentXElement element, MissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
ConfigElement = element;
ConfigElement = originalElement = element;
TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier);
VariantOf = element.VariantOf();
if (!VariantOf.IsEmpty) { return; } // Don't read the XML until the PrefabCollection loads the parent.
ParseConfigElement();
}
Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
public void InheritFrom(MissionPrefab parent)
{
ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement);
ParseConfigElement(parent);
}
Name = GetText(element.GetAttributeString("name", ""), "MissionName");
Description = GetText(element.GetAttributeString("description", ""), "MissionDescription");
private void ParseConfigElement(MissionPrefab variantOf = null)
{
TextIdentifier = ConfigElement.GetAttributeIdentifier("textidentifier", Identifier);
Tags = [.. ConfigElement.GetAttributeIdentifierArray("tags", [])];
Name = GetText(ConfigElement.GetAttributeString("name", ""), "MissionName");
Description = GetText(ConfigElement.GetAttributeString("description", ""), "MissionDescription");
LocalizedString GetText(string textTag, string textTagPrefix)
{
@@ -211,105 +235,100 @@ namespace Barotrauma
}
else
{
return
//prefer finding a text based on the specific text tag defined in the mission config
TextManager.Get(textTag)
//2nd option: the "default" format (MissionName.SomeMission)
.Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}"))
//last option: use the text in the xml as-is with no localization
.Fallback(textTag);
return TextManager.Get(textTag) // Prefer finding a text based on the specific text tag defined in the mission config.
.Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}")) // 2nd option: the "default" format (MissionName.SomeMission).
.Fallback(textTag); // Last option: Use the text in the xml as-is with no localization.
}
}
Reward = element.GetAttributeInt(nameof(Reward), 1);
ExperienceMultiplier = element.GetAttributeFloat(nameof(ExperienceMultiplier), 1.0f);
AllowRetry = element.GetAttributeBool(nameof(AllowRetry), false);
ShowSonarLabels = element.GetAttributeBool(nameof(ShowSonarLabels), true);
ShowInMenus = element.GetAttributeBool(nameof(ShowInMenus), true);
ShowStartMessage = element.GetAttributeBool(nameof(ShowStartMessage), true);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
Reward = ConfigElement.GetAttributeInt(nameof(Reward), 1);
ExperienceMultiplier = ConfigElement.GetAttributeFloat(nameof(ExperienceMultiplier), 1f);
AllowRetry = ConfigElement.GetAttributeBool(nameof(AllowRetry), false);
ShowSonarLabels = ConfigElement.GetAttributeBool(nameof(ShowSonarLabels), true);
ShowInMenus = ConfigElement.GetAttributeBool(nameof(ShowInMenus), true);
ShowStartMessage = ConfigElement.GetAttributeBool(nameof(ShowStartMessage), true);
IsSideObjective = ConfigElement.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false);
RequireThalamusWreck = element.GetAttributeBool(nameof(RequireThalamusWreck), false);
RequireRuin = element.GetAttributeBool(nameof(RequireRuin), false);
RequireBeaconStation = element.GetAttributeBool(nameof(RequireBeaconStation), false);
SpawnBeaconStationInMiddle = element.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false);
if (RequireThalamusWreck) { RequireWreck = true; }
RequireWreck = ConfigElement.GetAttributeBool(nameof(RequireWreck), false);
RequireThalamusWreck = ConfigElement.GetAttributeBool(nameof(RequireThalamusWreck), false);
RequireRuin = ConfigElement.GetAttributeBool(nameof(RequireRuin), false);
RequireBeaconStation = ConfigElement.GetAttributeBool(nameof(RequireBeaconStation), false);
SpawnBeaconStationInMiddle = ConfigElement.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false);
RequireWreck |= RequireThalamusWreck;
LoadSubmarines = element.GetAttributeBool(nameof(LoadSubmarines), true);
LoadSubmarines = ConfigElement.GetAttributeBool(nameof(LoadSubmarines), true);
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
Commonness = element.GetAttributeInt(nameof(Commonness), 1);
AllowOtherMissionsInLevel = element.GetAttributeBool(nameof(AllowOtherMissionsInLevel), true);
BlockLocationTypeChanges = ConfigElement.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
RequiredLocationFaction = ConfigElement.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
Commonness = ConfigElement.GetAttributeInt(nameof(Commonness), 1);
AllowOtherMissionsInLevel = ConfigElement.GetAttributeBool(nameof(AllowOtherMissionsInLevel), true);
if (element.GetAttribute("difficulty") != null)
if (ConfigElement.GetAttribute("difficulty") != null)
{
int difficulty = element.GetAttributeInt(nameof(Difficulty), MinDifficulty);
int difficulty = ConfigElement.GetAttributeInt(nameof(Difficulty), MinDifficulty);
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
MinLevelDifficulty = element.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty);
MaxLevelDifficulty = element.GetAttributeInt(nameof(MaxLevelDifficulty), MaxLevelDifficulty);
MinLevelDifficulty = ConfigElement.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty);
MaxLevelDifficulty = ConfigElement.GetAttributeInt(nameof(MaxLevelDifficulty), MaxLevelDifficulty);
MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100));
MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100);
AllowOutpostNPCs = element.GetAttributeBool(nameof(AllowOutpostNPCs), true);
ForceOutpostGenerationParameters = element.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty);
AllowOutpostSelectionFromTag = element.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty);
AllowOutpostNPCs = ConfigElement.GetAttributeBool(nameof(AllowOutpostNPCs), true);
ForceOutpostGenerationParameters = ConfigElement.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty);
AllowOutpostSelectionFromTag = ConfigElement.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty);
if (element.GetAttribute(nameof(ForceRespawnMode)) != null)
if (ConfigElement.GetAttribute(nameof(ForceRespawnMode)) != null)
{
ForceRespawnMode = element.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound);
ForceRespawnMode = ConfigElement.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound);
}
ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false);
ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false);
MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1);
string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), "");
ShowProgressBar = ConfigElement.GetAttributeBool(nameof(ShowProgressBar), false);
ShowProgressInNumbers = ConfigElement.GetAttributeBool(nameof(ShowProgressInNumbers), false);
MaxProgressState = ConfigElement.GetAttributeInt(nameof(MaxProgressState), 1);
string progressBarLabel = ConfigElement.GetAttributeString(nameof(ProgressBarLabel), "");
ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel);
string successMessageTag = element.GetAttributeString("successmessage", "");
string successMessageTag = ConfigElement.GetAttributeString("successmessage", "");
SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}");
if (!string.IsNullOrEmpty(successMessageTag))
{
SuccessMessage = SuccessMessage
.Fallback(TextManager.Get(successMessageTag))
.Fallback(successMessageTag);
.Fallback(TextManager.Get(successMessageTag))
.Fallback(successMessageTag);
}
SuccessMessage = SuccessMessage.Fallback(TextManager.Get("missioncompleted"));
string failureMessageTag = element.GetAttributeString("failuremessage", "");
string failureMessageTag = ConfigElement.GetAttributeString("failuremessage", "");
FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}");
if (!string.IsNullOrEmpty(failureMessageTag))
{
FailureMessage = FailureMessage
.Fallback(TextManager.Get(failureMessageTag))
.Fallback(failureMessageTag);
.Fallback(TextManager.Get(failureMessageTag))
.Fallback(failureMessageTag);
}
FailureMessage = FailureMessage.Fallback(TextManager.Get("missionfailed"));
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
SonarLabel =
TextManager.Get($"MissionSonarLabel.{sonarLabelTag}")
.Fallback(TextManager.Get(sonarLabelTag))
.Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}"));
string sonarLabelTag = ConfigElement.GetAttributeString("sonarlabel", "");
SonarLabel = TextManager.Get($"MissionSonarLabel.{sonarLabelTag}")
.Fallback(TextManager.Get(sonarLabelTag))
.Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}"));
if (!string.IsNullOrEmpty(sonarLabelTag))
{
SonarLabel = SonarLabel.Fallback(sonarLabelTag);
}
SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", "");
SonarIconIdentifier = ConfigElement.GetAttributeIdentifier("sonaricon", "");
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false);
MultiplayerOnly = ConfigElement.GetAttributeBool("multiplayeronly", false);
SingleplayerOnly = ConfigElement.GetAttributeBool("singleplayeronly", false);
AchievementIdentifier = element.GetAttributeIdentifier("achievementidentifier", "");
AchievementIdentifier = ConfigElement.GetAttributeIdentifier("achievementidentifier", "");
UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", Array.Empty<string>()).ToList();
UnhideEntitySubCategories = [.. ConfigElement.GetAttributeStringArray("unhideentitysubcategories", [])];
var headers = new List<LocalizedString>();
var messages = new List<LocalizedString>();
AllowedConnectionTypes = new List<(Identifier from, Identifier to)>();
List<LocalizedString> headers = [];
List<LocalizedString> messages = [];
AllowedConnectionTypes = [];
for (int i = 0; i < 100; i++)
{
@@ -322,26 +341,24 @@ namespace Barotrauma
}
}
List<ReputationReward> reputationRewards = new List<ReputationReward>();
List<ReputationReward> reputationRewards = [];
int messageIndex = 0;
foreach (var subElement in element.Elements())
foreach (ContentXElement subElement in ConfigElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "message":
if (messageIndex > headers.Count - 1)
if (messageIndex >= headers.Count)
{
headers.Add(string.Empty);
messages.Add(string.Empty);
}
headers[messageIndex] =
TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("header", "")))
.Fallback(subElement.GetAttributeString("header", ""));
messages[messageIndex] =
TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("text", "")))
.Fallback(subElement.GetAttributeString("text", ""));
headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("header", "")))
.Fallback(subElement.GetAttributeString("header", ""));
messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("text", "")))
.Fallback(subElement.GetAttributeString("text", ""));
messageIndex++;
break;
case "locationtype":
@@ -352,9 +369,7 @@ namespace Barotrauma
}
else
{
AllowedConnectionTypes.Add((
subElement.GetAttributeIdentifier("from", ""),
subElement.GetAttributeIdentifier("to", "")));
AllowedConnectionTypes.Add((subElement.GetAttributeIdentifier("from", ""), subElement.GetAttributeIdentifier("to", "")));
}
break;
case "locationtypechange":
@@ -375,7 +390,7 @@ namespace Barotrauma
string operatingString = subElement.GetAttributeString("operation", string.Empty);
if (!string.IsNullOrWhiteSpace(operatingString))
{
operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString);
operation = (SetDataAction.OperationType)Enum.Parse(typeof(SetDataAction.OperationType), operatingString);
}
DataRewards.Add((identifier, value, operation));
@@ -386,13 +401,13 @@ namespace Barotrauma
break;
}
}
Headers = headers.ToImmutableArray();
Messages = messages.ToImmutableArray();
ReputationRewards = reputationRewards.ToImmutableList();
Headers = [.. headers];
Messages = [.. messages];
ReputationRewards = [.. reputationRewards];
MissionClass = FindMissionClass(ConfigElement);
Type = ConfigElement.GetAttributeIdentifier(nameof(Type), Identifier.Empty);
MissionClass = FindMissionClass(element);
Type = element.GetAttributeIdentifier(nameof(Type), Identifier.Empty);
#if DEBUG
if (MissionClass == typeof(MonsterMission) && SonarLabel.IsNullOrEmpty())
{
@@ -403,17 +418,19 @@ namespace Barotrauma
if (!LoadSubmarines && MissionClass != typeof(CombatMission))
{
DebugConsole.AddWarning($"Potential error in mission {Identifier}: Disabling submarines is only intended for combat missions taking place in an outpost, and may lead to issues in other types of missions.",
contentPackage: element.ContentPackage);
contentPackage: ConfigElement.ContentPackage);
}
constructor = FindMissionConstructor(element, MissionClass);
constructor = FindMissionConstructor(ConfigElement, MissionClass);
if (constructor == null)
{
DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!",
contentPackage: element.ContentPackage);
contentPackage: ConfigElement.ContentPackage);
}
InitProjSpecific(element);
#if CLIENT
ParseConfigElementClient(ConfigElement, variantOf);
#endif
}
private Type FindMissionClass(ContentXElement element)
@@ -476,8 +493,6 @@ namespace Barotrauma
}
return constructor;
}
partial void InitProjSpecific(ContentXElement element);
public bool IsAllowed(Location from, Location to)
{

View File

@@ -64,6 +64,8 @@ namespace Barotrauma
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
if (GameMain.NetworkMember == null && monsterElement.GetAttributeBool("multiplayeronly", false)) { continue; }
speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty);
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)

View File

@@ -14,8 +14,8 @@ namespace Barotrauma
private readonly List<Item> items = new List<Item>();
private readonly Dictionary<Item, StatusEffect> statusEffectOnApproach = new Dictionary<Item, StatusEffect>();
//string = filename, point = min,max
private readonly HashSet<Tuple<CharacterPrefab, Point>> monsterPrefabs = new HashSet<Tuple<CharacterPrefab, Point>>();
//key = monster to spawn, point = min,max
private readonly List<Tuple<CharacterPrefab, Point>> monsterPrefabs = new List<Tuple<CharacterPrefab, Point>>();
private float itemSpawnRadius = 800.0f;
private readonly float approachItemsRadius = 1000.0f;
@@ -70,6 +70,8 @@ namespace Barotrauma
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
if (GameMain.NetworkMember == null && monsterElement.GetAttributeBool("multiplayeronly", false)) { continue; }
Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty);
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)

View File

@@ -300,7 +300,9 @@ namespace Barotrauma
return;
}
Submarine refSub = GetReferenceSub(acceptRemoteControlledSubs: true);
if (Submarine.MainSubs.Length == 2 && Submarine.MainSubs[1] != null)
//randomly choose which main sub to spawn the monsters around if there's 2 player subs (e.g. in the PvP mode)
if (Submarine.MainSubs.Length == 2 &&
Submarine.MainSubs[1] is { Info.Type: SubmarineType.Player })
{
refSub = Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced);
}

View File

@@ -89,7 +89,19 @@ namespace Barotrauma
private static async Task<AuthTicket> GetSteamAuthTicket()
{
var authTicket = await SteamManager.GetAuthTicketForGameAnalyticsConsent();
var authTicketTask = SteamManager.GetAuthTicketForGameAnalyticsConsent();
// Add a timeout to prevent the game from freezing indefinitely
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(10));
var completedTask = await Task.WhenAny(authTicketTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException("Timed out while trying to retrieve Steamworks authentication ticket for GameAnalytics.");
}
var authTicket = await authTicketTask;
return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 }
? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex
: throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics");

View File

@@ -690,7 +690,7 @@ namespace Barotrauma
foreach (Item containedItem in character.Inventory.AllItemsMod)
{
//only put into containers that draw the inventory (not ones with a hidden inventory like circuit boxes!)
if (containedItem.OwnInventory?.Container is { DrawInventory: true } &&
if (containedItem.OwnInventory?.Container is { DrawInventory: true } container && container.IsAccessible() &&
containedItem.OwnInventory.TryPutItem(item, user: null, item.AllowedSlots))
{
break;

View File

@@ -469,7 +469,8 @@ namespace Barotrauma
foreach (var mission in currentLocation.AvailableMissions)
{
//if the mission isn't shown in menus, it cannot be selected by the player -> must be something that is supposed to be automatically selected
if (!mission.Prefab.ShowInMenus)
//side objectives are also automatically selected
if (!mission.Prefab.ShowInMenus || mission.Prefab.IsSideObjective)
{
currentLocation.SelectMission(mission);
}
@@ -1429,18 +1430,18 @@ namespace Barotrauma
map = null;
}
public int NumberOfMissionsAtLocation(Location location)
public int NumberOfSelectableMissionsAtLocation(Location location)
{
return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location)) ?? 0;
return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location) && !m.Prefab.IsSideObjective) ?? 0;
}
public void CheckTooManyMissions(Location currentLocation, Client sender)
{
foreach (Location location in currentLocation.Connections.Select(c => c.OtherLocation(currentLocation)))
{
if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount)
if (NumberOfSelectableMissionsAtLocation(location) > Settings.TotalMaxMissionCount)
{
DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfSelectableMissionsAtLocation(location)}. Deselecting extra missions.");
foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList())
{
currentLocation.DeselectMission(mission);

View File

@@ -30,11 +30,23 @@ namespace Barotrauma
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionTypes, difficultyLevel: GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty);
float difficulty = GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty;
var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionTypes, difficultyLevel: difficulty);
if (mission == null)
{
DebugConsole.AddWarning(
$"Could not find any missions matching the mission types {string.Join(", ", missionTypes.Select(m => m.Value))} " +
$"and the difficulty {difficulty}. Ignoring the difficulty requirement...");
mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionTypes);
}
if (mission != null)
{
missions.Add(mission);
}
else
{
DebugConsole.AddWarning($"Could not find any missions matching the mission types {string.Join(", ", missionTypes.Select(m => m.Value))}.");
}
}
protected static IEnumerable<MissionPrefab> ValidateMissionPrefabs(IEnumerable<MissionPrefab> missionPrefabs, Dictionary<Identifier, Type> missionClasses)

View File

@@ -741,7 +741,7 @@ namespace Barotrauma
var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage);
if (missionsToShow.Count() > 1)
{
string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name));
string joinedMissionNames = string.Join(", ", missions.Where(static m => m.Prefab.ShowInMenus).Select(static m => m.Name));
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false);
}
else

View File

@@ -226,6 +226,14 @@ namespace Barotrauma
return false;
}
public bool IsSlotEmpty(InvSlotType limbSlot)
{
for (int i = 0; i < slots.Length; i++)
{
if (SlotTypes[i] == limbSlot && slots[i].Empty()) { return true; }
}
return false;
}
/// <summary>
/// Can the item be put in the inventory in a slot of the specified type (i.e. is there a suitable free slot or a stack the item can be put in).
@@ -438,7 +446,8 @@ namespace Barotrauma
}
int placedInSlot = -1;
foreach (InvSlotType allowedSlot in allowedSlots)
//order by whether the slot is empty, i.e. try putting in free slots first before trying to unequip items from occupied slots
foreach (InvSlotType allowedSlot in allowedSlots.OrderBy(slotType => IsSlotEmpty(slotType) ? 0 : 1))
{
if (allowedSlot.HasFlag(InvSlotType.RightHand) && character.AnimController.GetLimb(LimbType.RightHand) == null) { continue; }
if (allowedSlot.HasFlag(InvSlotType.LeftHand) && character.AnimController.GetLimb(LimbType.LeftHand) == null) { continue; }

View File

@@ -87,6 +87,13 @@ namespace Barotrauma.Items.Components
"Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")]
public DirectionType ForceDockingDirection { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Was the docking port docked at the end of the previous round.")]
public bool WasDocked
{
get;
set;
}
public DockingPort DockingTarget { get; private set; }
/// <summary>
@@ -280,6 +287,9 @@ namespace Barotrauma.Items.Components
OnDocked?.Invoke();
OnDocked = null;
WasDocked = true;
DockingTarget.Docked = true;
}
public void Lock(bool isNetworkMessage, bool applyEffects = true, bool moveSubs = true)
@@ -988,6 +998,8 @@ namespace Barotrauma.Items.Components
Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine);
obstructedWayPointsDisabled = false;
WasDocked = false;
DockingTarget.WasDocked = false;
DockingTarget.Undock();
DockingTarget = null;
@@ -1052,6 +1064,16 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
//PRETTY HACKY:
//the docking port was docked on the previous round, but not any more -
//must mean that whatever it was docked to (e.g. some enemy sub or respawn shuttle) no longer exists
//let's send an "on_undock" signal so circuits can react to the undocking that never "actually" happened
if (!docked && WasDocked)
{
item.SendSignal("1", "on_undock");
WasDocked = false;
}
dockingCooldown -= deltaTime;
if (DockingTarget == null)
{
@@ -1208,19 +1230,21 @@ namespace Barotrauma.Items.Components
}
}
if (!item.linkedTo.Any()) { return; }
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
foreach (MapEntity entity in linked)
{
if (!(entity is Item linkedItem)) { continue; }
var dockingPort = linkedItem.GetComponent<DockingPort>();
if (dockingPort != null)
if (item.linkedTo.Any())
{
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
foreach (MapEntity entity in linked)
{
Dock(dockingPort);
}
if (entity is not Item linkedItem) { continue; }
var dockingPort = linkedItem.GetComponent<DockingPort>();
if (dockingPort != null)
{
Dock(dockingPort);
}
}
}
}
public override void ReceiveSignal(Signal signal, Connection connection)

View File

@@ -154,6 +154,13 @@ namespace Barotrauma.Items.Components
}
}
[Serialize("0,0", IsPropertySaveable.Yes)]
public Point DisallowAttachingOverSize
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
public bool AttachedByDefault
{
@@ -496,13 +503,19 @@ namespace Barotrauma.Items.Components
Vector2 diff = new Vector2(
(heldHand.SimPosition.X - arm.SimPosition.X) / 2f,
(heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f);
item.SetTransform(heldHand.SimPosition + diff, 0.0f);
//we have forced the item to be in the same sub as the dropper above,
//and are placing it to the position of the hands in "local" coordinates
//which may be outside the sub if the character is e.g. standing half-way through the airlock
// -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space,
// or it will end up in a weird state and seemingly disappear
item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine);
}
else
{
item.SetTransform(picker.SimPosition, 0.0f);
}
}
item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine);
}
}
}
picker.Inventory.RemoveItem(item);
@@ -621,17 +634,34 @@ namespace Barotrauma.Items.Components
if (disallowAttachingOverTags.Any() || !AllowAttachInsideDoors)
{
var connectedHulls = item.CurrentHull?.GetConnectedHulls(includingThis: true, searchDepth: 5, ignoreClosedGaps: true);
Vector2 size = item.Rect.Size.ToVector2() / 2;
Vector2 size = DisallowAttachingOverSize == Point.Zero ?
item.Rect.Size.ToVector2() :
DisallowAttachingOverSize.ToVector2() * item.Scale;
size /= 2f;
foreach (Item otherItem in Item.ItemList)
{
if (otherItem == item || otherItem.body is { BodyType: BodyType.Dynamic, Enabled: true }) { continue; }
if (connectedHulls != null && !connectedHulls.Contains(otherItem.CurrentHull)) { continue; }
if (disallowAttachingOverTags.None(tag => otherItem.HasTag(tag)) &&
if (disallowAttachingOverTags.None(otherItem.HasTag) &&
(otherItem.GetComponent<Door>() == null || AllowAttachInsideDoors))
{
continue;
}
Rectangle worldRect = otherItem.WorldRect;
if (otherItem.GetComponent<Holdable>() is Holdable otherHoldable)
{
if (!otherHoldable.attached) { continue; }
if (otherHoldable.DisallowAttachingOverSize != Point.Zero)
{
Vector2 scaledSize = otherHoldable.DisallowAttachingOverSize.ToVector2() * item.Scale;
worldRect = new Rectangle(
otherItem.WorldPosition.ToPoint() - new Point((int)(scaledSize.X / 2), (int)(-scaledSize.Y / 2)),
scaledSize.ToPoint());
}
}
if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; }
if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; }
tempOverlappingItems.Add(otherItem);

View File

@@ -539,8 +539,8 @@ namespace Barotrauma.Items.Components
}
if (targetEntity != null)
{
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, attackMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, attackMultiplier: damageMultiplier);
}
if (DeleteOnUse)

View File

@@ -922,7 +922,8 @@ namespace Barotrauma.Items.Components
}
}
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f)
/// <param name="attackMultiplier">Multiplier used on afflictions caused by the status effects, except ones that <see cref="AfflictionPrefab.AffectedByAttackMultipliers">have been configured to not be affected by attack multipliers.</see></param>
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float attackMultiplier = 1.0f)
{
if (statusEffectLists == null) { return; }
@@ -934,7 +935,7 @@ namespace Barotrauma.Items.Components
{
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
if (user != null) { effect.SetUser(user); }
effect.AfflictionMultiplier = afflictionMultiplier;
effect.AttackMultiplier = attackMultiplier;
var c = character;
if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
@@ -942,7 +943,7 @@ namespace Barotrauma.Items.Components
c = user;
}
item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
effect.AfflictionMultiplier = 1.0f;
effect.AttackMultiplier = 1.0f;
reducesCondition |= effect.ReducesItemCondition();
}
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well

View File

@@ -832,7 +832,7 @@ namespace Barotrauma.Items.Components
public float FabricationDegreeOfSuccess(Character character, ImmutableArray<Skill> skills)
{
if (skills.Length == 0) { return 1.0f; }
if (skills.Length == 0) { return 0.5f; }
if (character == null) { return 0.0f; }
float minDegreeOfSuccess = 1.0f;

View File

@@ -0,0 +1,46 @@
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
namespace Barotrauma.Items.Components
{
/// <summary>
/// Makes the item inherit the condition from a linked wall or multiple - or in other words, makes it essentially treat the health of the wall as its own health.
/// The wall section with the most damage determines the condition (i.e. the item will be fully broken if there's at least one fully broken wall section).
/// </summary>
class InheritConditionFromLinkedWall(Item item, ContentXElement element) : ItemComponent(item, element)
{
private readonly List<Structure> linkedWalls = [];
public override void OnMapLoaded()
{
foreach (var linkedTo in item.linkedTo)
{
if (linkedTo is Structure structure &&
structure.HasBody)
{
linkedWalls.Add(structure);
structure.OnHealthChanged += (_, _) => UpdateCondition();
}
}
if (linkedWalls.None())
{
DebugConsole.AddWarning($"The item {item.Name} ({item.Prefab.Identifier}) is not linked to any walls with a physics body. The {nameof(InheritConditionFromLinkedWall)} component will do nothing.");
}
}
private void UpdateCondition()
{
float lowestHealthPercent = 1.0f;
foreach (var wall in linkedWalls)
{
foreach (var section in wall.Sections)
{
lowestHealthPercent = Math.Min(lowestHealthPercent, 1.0f - section.damage / wall.MaxHealth);
}
}
item.Condition = item.MaxCondition * lowestHealthPercent;
}
}
}

View File

@@ -136,6 +136,7 @@ namespace Barotrauma.Items.Components
item.CurrentHull.GetLinkedHulls(linkedHulls, includeHiddenHulls: true);
foreach (var linkedHull in linkedHulls)
{
if (linkedHull == item.CurrentHull) { continue; }
hullWaterVolume += linkedHull.WaterVolume;
totalHullVolume += linkedHull.Volume;
}
@@ -148,7 +149,7 @@ namespace Barotrauma.Items.Components
if (!IsActive || Disabled) { return; }
if (flowPercentage <= 0f && item.CurrentHull.WaterVolume <= 0f) { return; }
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
float powerFactor = Math.Min(PowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * MaxFlow * powerFactor;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)

View File

@@ -247,9 +247,13 @@ namespace Barotrauma.Items.Components
}
}
bool fissionRateControlledBySignals = signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1;
bool turbineOutputRateControlledBySignals = signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1;
//rapidly adjust the reactor in the first few seconds of the round to prevent overvoltages if the load changed between rounds
//(unless the reactor is being operated by a player)
if (GameMain.GameSession is { RoundDuration: <5 } && lastUser is not { IsPlayer: true })
if (GameMain.GameSession is { RoundDuration: < 5 } && lastUser is not { IsPlayer: true } && PowerOn && AutoTemp &&
!fissionRateControlledBySignals && !turbineOutputRateControlledBySignals)
{
UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
}
@@ -263,7 +267,7 @@ namespace Barotrauma.Items.Components
float maxPowerOut = GetMaxOutput();
if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1)
if (fissionRateControlledBySignals)
{
TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f);
#if CLIENT
@@ -274,7 +278,7 @@ namespace Barotrauma.Items.Components
{
signalControlledTargetFissionRate = null;
}
if (signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1)
if (turbineOutputRateControlledBySignals)
{
TargetTurbineOutput = adjustValueWithoutOverShooting(TargetTurbineOutput, signalControlledTargetTurbineOutput.Value, deltaTime * 5.0f);
#if CLIENT

View File

@@ -641,8 +641,7 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < hits.Count; i++)
{
var h = hits[i];
item.SetTransform(h.Point, rotation);
item.Submarine = h.Submarine;
item.SetTransform(h.Point, rotation, forceSubmarine: h.Submarine);
item.UpdateTransform();
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{

View File

@@ -103,7 +103,7 @@ namespace Barotrauma.Items.Components
var inputBuilder = ImmutableArray.CreateBuilder<CircuitBoxInputConnection>();
var outputBuilder = ImmutableArray.CreateBuilder<CircuitBoxOutputConnection>();
foreach (Connection conn in Item.Connections)
foreach (Connection conn in Item.Connections.OrderBy(static c => c.DisplayOrder))
{
if (conn.IsOutput)
{
@@ -236,9 +236,7 @@ namespace Barotrauma.Items.Components
cloneNode.ReplaceAllConnectionLabelOverrides(origNode.ConnectionLabelOverrides);
}
if (!clonedContainedItems.Any()) { return; }
foreach (var origComp in original.Components)
foreach (CircuitBoxComponent origComp in original.Components)
{
if (!clonedContainedItems.TryGetValue(origComp.Item.ID, out var clonedItem)) { continue; }
var newComponent = new CircuitBoxComponent(origComp.ID, clonedItem, origComp.Position, this, origComp.UsedResource);
@@ -661,6 +659,7 @@ namespace Barotrauma.Items.Components
}
wire.From.Connection.CircuitBoxConnections.Remove(wire.To);
wire.To.Connection.CircuitBoxConnections.Remove(wire.From);
if (wire.From is CircuitBoxInputConnection input)
{

View File

@@ -17,6 +17,8 @@ namespace Barotrauma.Items.Components
//how many wires can be linked to this connection in total
public readonly int MaxWires = 5;
public readonly int DisplayOrder;
public readonly string Name;
private readonly LocalizedString _displayName;
public LocalizedString DisplayName
@@ -92,7 +94,7 @@ namespace Barotrauma.Items.Components
return "Connection (" + item.Name + ", " + Name + ")";
}
public Connection(ContentXElement element, ConnectionPanel connectionPanel, IdRemap idRemap)
public Connection(ContentXElement element, int connectionIndex, ConnectionPanel connectionPanel, IdRemap idRemap, bool isItemSwap)
{
#if CLIENT
@@ -117,25 +119,44 @@ namespace Barotrauma.Items.Components
IsOutput = element.Name.ToString() == "output";
Name = element.GetAttributeString("name", IsOutput ? "output" : "input");
int displayOrder;
if (element.GetAttribute("displayorderoverride") is not { } displayOrderAttr)
{
var sameElements = connectionPanel.Connections.Where(c => c.IsOutput == IsOutput);
displayOrder = !sameElements.Any() ? 0 : sameElements.Max(static c => c.DisplayOrder) + 1;
}
else
{
displayOrder = displayOrderAttr.GetAttributeInt(0);
}
DisplayOrder = displayOrder;
string displayNameTag = "", fallbackTag = "";
//if displayname is not present, attempt to find it from the prefab
if (element.GetAttribute("displayname") == null)
{
foreach (var subElement in item.Prefab.ConfigElement.Elements())
{
if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }
if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }
int prefabConnectionIndex = 0;
foreach (XElement connectionElement in subElement.Elements())
{
string prefabConnectionName = connectionElement.GetAttributeString("name", null);
if (prefabConnectionName.IsNullOrEmpty()) { continue; }
string[] aliases = connectionElement.GetAttributeStringArray("aliases", Array.Empty<string>());
if (prefabConnectionName == Name || aliases.Contains(Name))
if (prefabConnectionName == Name || aliases.Contains(Name) ||
//when swapping items, we move wires based on the order of the connections, not the names
//= we should find a connection based on the index if the name doesn't match
(isItemSwap && connectionIndex == prefabConnectionIndex))
{
displayNameTag = connectionElement.GetAttributeString("displayname", "");
fallbackTag = connectionElement.GetAttributeString("fallbackdisplayname", "");
}
prefabConnectionIndex++;
}
}
}
}
else
{

View File

@@ -78,10 +78,10 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString())
{
case "input":
Connections.Add(new Connection(subElement, this, IdRemap.DiscardId));
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
break;
case "output":
Connections.Add(new Connection(subElement, this, IdRemap.DiscardId));
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
break;
}
}
@@ -293,10 +293,10 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString())
{
case "input":
loadedConnections.Add(new Connection(subElement, this, idRemap));
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
break;
case "output":
loadedConnections.Add(new Connection(subElement, this, idRemap));
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
break;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,7 +8,7 @@ namespace Barotrauma.Items.Components;
/// <summary>
/// Base class for signal components that can select between input/output connections (e.g. multiplexer and demultiplexer components)
/// </summary>
abstract class ConnectionSelectorComponent : ItemComponent
abstract partial class ConnectionSelectorComponent : ItemComponent, IServerSerializable
{
protected int selectedConnectionIndex;
protected string selectedConnectionIndexStr;
@@ -22,6 +23,8 @@ abstract class ConnectionSelectorComponent : ItemComponent
get { return selectedConnectionIndex; }
set
{
int prevIndex = selectedConnectionIndex; // store original, so we know if the state has changed and can sync it in MP
selectedConnectionIndex = Math.Max(0, value);
//don't clamp until we've determined how many connections the item has
//(can't be done until the connection panel component has been loaded too)
@@ -31,6 +34,11 @@ abstract class ConnectionSelectorComponent : ItemComponent
}
selectedConnectionName = GetConnectionName(selectedConnectionIndex);
selectedConnectionIndexStr = selectedConnectionIndex.ToString();
if (prevIndex != selectedConnectionIndex)
{
OnStateChanged();
}
}
}
@@ -55,6 +63,8 @@ abstract class ConnectionSelectorComponent : ItemComponent
{
}
partial void OnStateChanged();
protected abstract string GetConnectionName(int connectionIndex);
/// <summary>

View File

@@ -63,10 +63,7 @@ namespace Barotrauma.Items.Components
/// This can be used to make them additionally work the other way around, periodically getting the current value of the property from the item and refreshing the UI.
/// </summary>
public float GetValueInterval { get; set; } = -1.0f;
#if CLIENT
public float GetValueTimer;
#endif
public string Name => "CustomInterfaceElement";
@@ -248,7 +245,7 @@ namespace Barotrauma.Items.Components
ciElement.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal == ciElement.ContinuousSignal);
}
customInterfaceElementList.Add(ciElement);
IsActive |= ciElement.ContinuousSignal;
IsActive |= ciElement.ContinuousSignal || ciElement.GetValueInterval > 0.0f;
}
InitProjSpecific();
@@ -348,13 +345,10 @@ namespace Barotrauma.Items.Components
//make sure the clients know about the states of the checkboxes and text fields
if (customInterfaceElementList.Any())
{
if (item.FullyInitialized)
CoroutineManager.Invoke(() =>
{
CoroutineManager.Invoke(() =>
{
if (!item.Removed) { item.CreateServerEvent(this); }
}, delay: 0.1f);
}
if (item.FullyInitialized && !item.Removed) { item.CreateServerEvent(this); }
}, delay: 0.1f);
}
#endif
}
@@ -418,6 +412,16 @@ namespace Barotrauma.Items.Components
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
{
if (ciElement.GetValueInterval > 0.0f)
{
ciElement.GetValueTimer -= deltaTime;
if (ciElement.GetValueTimer <= 0.0f)
{
SetSignalToPropertyValue(ciElement);
ciElement.GetValueTimer = ciElement.GetValueInterval;
}
}
if (!ciElement.ContinuousSignal && ciElement.PropertyName != "Voltage") { continue; }
//TODO: allow changing output when a tickbox is not selected
if (!string.IsNullOrEmpty(ciElement.Signal) && ciElement.Connection != null)

View File

@@ -140,7 +140,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")]
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates. 0 = not at all, 1 = alternates between full brightness and off.")]
public float PulseAmount
{
get { return pulseAmount; }

View File

@@ -268,7 +268,7 @@ namespace Barotrauma.Items.Components
public float RotationSpeedHighSkill { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A)."),
Editable]
Editable(TransferToSwappedItem = true)]
public Color HudTint { get; set; }
[Header(localizedTextTag: "sp.turret.AutoOperate.propertyheader")]

View File

@@ -462,10 +462,25 @@ namespace Barotrauma
}
}
public float ImpactTolerance => Prefab.ImpactTolerance;
private float impactTolerance;
[Serialize(0.0f, IsPropertySaveable.No), ConditionallyEditable(ConditionallyEditable.ConditionType.ReceivesSubmarineImpacts, MinValueFloat = 0, MaxValueFloat = 100)]
public float ImpactTolerance
{
get { return impactTolerance; }
set { impactTolerance = Math.Max(value, 0.0f); }
}
public float ImpactDamage => Prefab.ImpactDamage;
public float ImpactDamageProbability => Prefab.ImpactDamageProbability;
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of damage the item takes from impacts. Acts as a multiplier on the strength of the impact. Note that ImpactTolerance must be set for impacts to register."),
ConditionallyEditable(ConditionallyEditable.ConditionType.ReceivesSubmarineImpacts, MinValueFloat = 0, MaxValueFloat = 100)]
public float ImpactDamage { get; set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "Probability for impacts to register. Defaults to 1. Note that ImpactTolerance must also be set for impacts to register."),
ConditionallyEditable(ConditionallyEditable.ConditionType.ReceivesSubmarineImpacts, MinValueFloat = 0, MaxValueFloat = 1)]
public float ImpactDamageProbability { get; set; }
public const float SubmarineImpactCooldown = 0.1f;
public double LastSubmarineImpactTime;
public float InteractDistance => Prefab.InteractDistance;
@@ -1559,7 +1574,7 @@ namespace Barotrauma
if (!updateableComponents.Contains(component))
{
updateableComponents.Add(component);
this.isActive = true;
this.IsActive = true;
}
}
};
@@ -1650,7 +1665,19 @@ namespace Barotrauma
contained.Container = null;
}
public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true)
/// <summary>
/// Sets the position and rotation of the item, and its physics body if it has one.
/// </summary>
/// <param name="simPosition">Position in simulation units.</param>
/// <param name="rotation">Rotation in radians</param>
/// <param name="findNewHull">Should the hull the item is inside be immediately updated? Generally only useful to set to false
/// for performance reasons when finding the hull is unnecessary (e.g. if it's being forced to something after setting the transform).</param>
/// <param name="setPrevTransform">Should the previous transform of the item be immediately set to the new one?
/// The previous transform is used to interpolate draw positions/rotations, and you should generally only set this to false if
/// you're trying to simulate movement instead of simply teleporting the item somewhere.</param>
/// <param name="forceSubmarine">If you know the position is in a specific sub's coordinate space and want to ensure the item
/// is still considered to be in that sub even if the transform ended up outside hulls.</param>
public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true, Submarine forceSubmarine = null)
{
if (!MathUtils.IsValid(simPosition))
{
@@ -1688,6 +1715,7 @@ namespace Barotrauma
rect.Y = (int)MathF.Round(displayPos.Y + rect.Height / 2.0f);
if (findNewHull) { FindHull(); }
if (forceSubmarine != null) { Submarine = forceSubmarine; }
}
/// <summary>
@@ -1859,7 +1887,7 @@ namespace Barotrauma
if (newRootContainer != RootContainer)
{
RootContainer = newRootContainer;
isActive = true;
IsActive = true;
foreach (Item containedItem in ContainedItems)
{
containedItem.RefreshRootContainer();
@@ -2374,12 +2402,16 @@ namespace Barotrauma
}
}
private bool isActive = true;
/// <summary>
/// Inactive items are not updated. Note that actions such as dropping can reactivate the item, and that the item can go inactive by itself if it no longer needs updating;
/// </summary>
public bool IsActive = true;
public bool IsInRemoveQueue;
public override void Update(float deltaTime, Camera cam)
{
if (!isActive || IsLayerHidden || IsInRemoveQueue) { return; }
if (!IsActive || IsLayerHidden || IsInRemoveQueue) { return; }
if (impactQueue != null)
{
@@ -2545,7 +2577,7 @@ namespace Barotrauma
#if CLIENT
positionBuffer.Clear();
#endif
isActive = false;
IsActive = false;
}
}
@@ -2706,7 +2738,7 @@ namespace Barotrauma
impactQueue.Enqueue(impact);
}
isActive = true;
IsActive = true;
return true;
}
@@ -3496,7 +3528,7 @@ namespace Barotrauma
if (body != null)
{
isActive = true;
IsActive = true;
body.Enabled = true;
body.PhysEnabled = true;
body.ResetDynamics();
@@ -3636,7 +3668,7 @@ namespace Barotrauma
item.body.Enabled = item.body.PhysEnabled = isFirst;
if (isFirst)
{
item.isActive = true;
item.IsActive = true;
item.body.ResetDynamics();
}
}
@@ -4401,13 +4433,18 @@ namespace Barotrauma
{
foreach (var connection in thisConnectionPanel.Connections)
{
var newConnection = newConnectionPanel.Connections.FirstOrDefault(c => c.Name == connection.Name);
if (newConnection == null) { continue; }
foreach (var wire in connection.Wires)
{
int connectionIndex = wire.Connections.IndexOf(connection);
int wireConnectionIndex = wire.Connections.IndexOf(connection);
wire.RemoveConnection(this);
wire.Connect(newConnection, connectionIndex, addNode: false);
int thisConnectionIndex = connection.ConnectionPanel.Connections.IndexOf(connection);
if (thisConnectionIndex < 0 || thisConnectionIndex >= newConnectionPanel.Connections.Count)
{
DebugConsole.AddWarning($"Failed to move a wire from the connection {connection.Name} when swapping the item {Name} with {newItem.Name}. The new item probably does not have the same number of connections as the previous one.");
continue;
}
Connection newConnection = newConnectionPanel.Connections[thisConnectionIndex];
wire.Connect(newConnection, wireConnectionIndex, addNode: false);
newConnection.ConnectWire(wire);
}
}

View File

@@ -813,20 +813,6 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool DamagedByMonsters { get; private set; }
private float impactTolerance;
[Serialize(0.0f, IsPropertySaveable.No)]
public float ImpactTolerance
{
get { return impactTolerance; }
set { impactTolerance = Math.Max(value, 0.0f); }
}
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of damage the item takes from impacts. Acts as a multiplier on the strength of the impact. Note that ImpactTolerance must be set for impacts to register.")]
public float ImpactDamage { get; set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "Probability for impacts to register. Defaults to 1. Note that ImpactTolerance must also be set for impacts to register.")]
public float ImpactDamageProbability { get; set; }
[Serialize(false, IsPropertySaveable.No, "If true, submarine impacts will trigger OnImpact effects. Only applies to items with a null or non-dynamic physics body - items with dynamic bodies always react to impacts.")]
public bool ReceiveSubmarineImpacts { get; set; }

View File

@@ -404,7 +404,7 @@ namespace Barotrauma
return;
}
DamageCharacters(worldPosition, Attack, force, damageSource, attacker);
DamageCharacters(worldPosition, Attack, force, damageSource, attacker, displayRange);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -465,12 +465,12 @@ namespace Barotrauma
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, float range)
{
if (attack.Range <= 0.0f) { return; }
if (range <= 0.0f) { return; }
//long range for the broad distance check, because large characters may still be in range even if their collider isn't
float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f);
float broadRange = Math.Max(range * 10.0f, 10000.0f);
foreach (Character c in Character.CharacterList)
{
@@ -518,7 +518,7 @@ namespace Barotrauma
float limbRadius = limb.body.GetMaxExtent();
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
if (dist > attack.Range) { continue; }
if (dist > range) { continue; }
float distFactor =
DistanceFalloff ?

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