Build 0.21.1.0

This commit is contained in:
Markus Isberg
2023-01-13 18:10:35 +02:00
parent 2f7205fb4b
commit 697ec52120
155 changed files with 2423 additions and 1237 deletions
@@ -107,7 +107,7 @@ namespace Barotrauma
Collider.AngularVelocity = newAngularVelocity; Collider.AngularVelocity = newAngularVelocity;
float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition);
float errorTolerance = character.CanMove ? 0.01f : 0.2f; float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f;
if (distSqrd > errorTolerance) if (distSqrd > errorTolerance)
{ {
if (distSqrd > 10.0f || !character.CanMove) if (distSqrd > 10.0f || !character.CanMove)
@@ -145,6 +145,7 @@ namespace Barotrauma
{ {
MainLimb.PullJointWorldAnchorB = Collider.SimPosition; MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointEnabled = true; MainLimb.PullJointEnabled = true;
MainLimb.body.LinearVelocity = newVelocity;
} }
} }
} }
@@ -442,10 +443,20 @@ namespace Barotrauma
{ {
foreach (Limb limb in Limbs) foreach (Limb limb in Limbs)
{ {
if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; } if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; }
Vector2 spriteOrigin = limb.ActiveSprite.Origin;
spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; FlipSprite(limb.DeformSprite?.Sprite ?? limb.Sprite);
limb.ActiveSprite.Origin = spriteOrigin; foreach (var conditionalSprite in limb.ConditionalSprites)
{
FlipSprite(conditionalSprite.DeformableSprite?.Sprite ?? conditionalSprite.Sprite);
}
}
static void FlipSprite(Sprite sprite)
{
if (sprite == null) { return; }
Vector2 spriteOrigin = sprite.Origin;
spriteOrigin.X = sprite.SourceRect.Width - spriteOrigin.X;
sprite.Origin = spriteOrigin;
} }
} }
@@ -616,7 +616,7 @@ namespace Barotrauma
return closestItem; return closestItem;
} }
private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = 150.0f) private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = MaxHighlightDistance)
{ {
Character closestCharacter = null; Character closestCharacter = null;
@@ -626,7 +626,7 @@ namespace Barotrauma
{ {
if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; }
float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition); float dist = c.GetDistanceToClosestLimb(mouseSimPos);
if (dist < closestDist || if (dist < closestDist ||
(c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist)) (c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist))
{ {
@@ -608,7 +608,8 @@ namespace Barotrauma
} }
} }
Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) * 0.7f; float dist = Vector2.Distance(character.FocusedCharacter.DrawPosition, character.DrawPosition);
Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) / dist * Math.Min(dist, Character.MaxDragDistance);
startPos = cam.WorldToScreen(startPos); startPos = cam.WorldToScreen(startPos);
string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName;
@@ -938,7 +938,7 @@ namespace Barotrauma
var headPreset = obj as HeadPreset; var headPreset = obj as HeadPreset;
if (info.Head.Preset != headPreset) if (info.Head.Preset != headPreset)
{ {
info.Head = new HeadInfo(info, headPreset) info.Head = new HeadInfo(info, headPreset, info.Head.HairIndex, info.Head.BeardIndex, info.Head.MoustacheIndex, info.Head.FaceAttachmentIndex)
{ {
SkinColor = info.Head.SkinColor, SkinColor = info.Head.SkinColor,
HairColor = info.Head.HairColor, HairColor = info.Head.HairColor,
@@ -640,8 +640,7 @@ namespace Barotrauma
else else
{ {
forceAfflictionContainerUpdate = true; forceAfflictionContainerUpdate = true;
currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true) currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null);
.FindAll(a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null);
currentDisplayedAfflictions.Sort((a1, a2) => currentDisplayedAfflictions.Sort((a1, a2) =>
{ {
int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond); int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond);
@@ -1275,7 +1274,7 @@ namespace Barotrauma
//displaying an affliction we no longer have -> dirty //displaying an affliction we no longer have -> dirty
foreach ((Affliction affliction, float strength) in displayedAfflictions) foreach ((Affliction affliction, float strength) in displayedAfflictions)
{ {
if (!afflictions.Any(a => a.Key == affliction)) { return true; } if (afflictions.None(a => a.Key == affliction && a.Key.ShouldShowIcon(Character))) { return true; }
} }
return false; return false;
} }
@@ -1766,7 +1766,7 @@ namespace Barotrauma
{ {
foreach (UpgradePrefab prefab in categoryData.Prefabs) foreach (UpgradePrefab prefab in categoryData.Prefabs)
{ {
var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false); var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false).Frame;
UpgradeStore.UpdateUpgradeEntry(frame, prefab, categoryData.Category, campaign); UpgradeStore.UpdateUpgradeEntry(frame, prefab, categoryData.Category, campaign);
} }
} }
@@ -796,7 +796,7 @@ namespace Barotrauma
CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo; CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo;
if (ownCharacterInfo is null) { return false; } if (ownCharacterInfo is null) { return false; }
return info == ownCharacterInfo; return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName();
} }
public static bool CanManageTalents(CharacterInfo targetInfo) public static bool CanManageTalents(CharacterInfo targetInfo)
@@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.Linq; using System.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
@@ -90,6 +89,16 @@ namespace Barotrauma
Repairs Repairs
} }
private enum UpgradeStoreUserData
{
BuyButton,
BuyButtonLayout,
ProgressBarLayout,
IncreaseLabel,
PriceLabel,
MaterialCostList
}
public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) public UpgradeStore(CampaignUI campaignUI, GUIComponent parent)
{ {
WaitForServerUpdate = false; WaitForServerUpdate = false;
@@ -600,7 +609,7 @@ namespace Barotrauma
GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true };
new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true };
new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price)); new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price));
GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = UpgradeStoreUserData.BuyButtonLayout };
new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed };
contentLayout.Recalculate(); contentLayout.Recalculate();
buyButtonLayout.Recalculate(); buyButtonLayout.Recalculate();
@@ -950,7 +959,7 @@ namespace Barotrauma
frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite,
item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity),
currentOrPending.Description, currentOrPending.Description,
0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton")); 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame);
if (canUninstall && frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton refundButton) if (canUninstall && frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton refundButton)
{ {
@@ -987,11 +996,11 @@ namespace Barotrauma
int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count();
frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description,
price, replacement, price, replacement,
addBuyButton: true, addBuyButton: true,
addProgressBar: false, addProgressBar: false,
buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton").Frame);
if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; }
if (PlayerBalance >= price) if (PlayerBalance >= price)
@@ -1081,13 +1090,23 @@ namespace Barotrauma
}; };
} }
public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) public readonly record struct BuyButtonFrame(GUILayoutGroup Layout, GUIListBox MaterialCostList, GUIButton BuyButton, GUITextBlock PriceText);
public readonly record struct ProgressBarFrame(GUITextBlock ProgressText, GUIProgressBar ProgressBar);
public readonly record struct UpgradeFrame(GUIFrame Frame,
GUIImage Icon,
GUITextBlock Name,
GUITextBlock Description,
Option<BuyButtonFrame> BuyButton,
Option<ProgressBarFrame> ProgressBar);
public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true)
{ {
int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category));
} }
public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0) public static UpgradeFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0)
{ {
float progressBarHeight = 0.25f; float progressBarHeight = 0.25f;
@@ -1105,21 +1124,26 @@ namespace Barotrauma
* |------------------------------------------------------------------| * |------------------------------------------------------------------|
*/ */
GUIFrame prefabFrame = new GUIFrame(parent, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = userData }; GUIFrame prefabFrame = new GUIFrame(parent, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = userData };
GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: true) { Stretch = true }; GUILayoutGroup mainLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: false);
GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(1f, addBuyButton ? 0.65f : 1f, mainLayout, Anchor.Center), isHorizontal: true) { Stretch = true };
var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center);
GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false };
var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout));
GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero };
var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout));
GUILayoutGroup? progressLayout = null; var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero };
GUILayoutGroup? progressLayout = null;
GUILayoutGroup? buyButtonLayout = null; GUILayoutGroup? buyButtonLayout = null;
Option<BuyButtonFrame> buyButtonOption = Option<BuyButtonFrame>.None();
Option<ProgressBarFrame> progressBarOption = Option<ProgressBarFrame>.None();
if (addProgressBar) if (addProgressBar)
{ {
progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = UpgradeStoreUserData.ProgressBarLayout };
new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); GUITextBlock progressText = new GUITextBlock(rectT(0.15f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero };
new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; GUIProgressBar progressBar = new GUIProgressBar(rectT(0.85f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange);
progressBarOption = Option.Some(new ProgressBarFrame(progressText, progressBar));
} }
if (addBuyButton) if (addBuyButton)
@@ -1127,12 +1151,33 @@ namespace Barotrauma
var formattedPrice = TextManager.FormatCurrency(Math.Abs(price)); var formattedPrice = TextManager.FormatCurrency(Math.Abs(price));
//negative price = refund //negative price = refund
if (price < 0) { formattedPrice = "+" + formattedPrice; } if (price < 0) { formattedPrice = "+" + formattedPrice; }
buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; buyButtonLayout = new GUILayoutGroup(rectT(1f, 0.35f, mainLayout), isHorizontal: true) { UserData = UpgradeStoreUserData.BuyButtonLayout };;
var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center)
GUIListBox materialCostList;
if (upgradePrefab is not null)
{ {
var increaseText = new GUITextBlock(rectT(imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), "", textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont)
{
UserData = UpgradeStoreUserData.IncreaseLabel
};
UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel);
materialCostList = new GUIListBox(rectT(0.65f - imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), isHorizontal: true, style: null);
}
else
{
materialCostList = new GUIListBox(rectT(0.65f, 1f, buyButtonLayout), isHorizontal: true, style: null);
}
materialCostList.Visible = false;
materialCostList.UserData = UpgradeStoreUserData.MaterialCostList;
var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice)
{
UserData = UpgradeStoreUserData.PriceLabel,
//prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades
Visible = userData is ItemPrefab Visible = userData is ItemPrefab
}; };
if (price < 0) if (price < 0)
{ {
priceText.TextColor = GUIStyle.Green; priceText.TextColor = GUIStyle.Green;
@@ -1141,15 +1186,13 @@ namespace Barotrauma
{ {
priceText.Text = string.Empty; priceText.Text = string.Empty;
} }
new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) GUIButton buyButton = new GUIButton(rectT(0.15f, 1f, buyButtonLayout), string.Empty, style: buttonStyle)
{ {
UserData = UpgradeStoreUserData.BuyButton,
Enabled = false Enabled = false
}; };
if (upgradePrefab != null)
{ buyButtonOption = Option.Some(new BuyButtonFrame(buyButtonLayout, materialCostList, buyButton, priceText));
var increaseText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), "", textAlignment: Alignment.Center);
UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel);
}
} }
description.CalculateHeightFromText(); description.CalculateHeightFromText();
@@ -1175,7 +1218,7 @@ namespace Barotrauma
progressLayout?.Recalculate(); progressLayout?.Recalculate();
buyButtonLayout?.Recalculate(); buyButtonLayout?.Recalculate();
return prefabFrame; return new UpgradeFrame(prefabFrame, icon, name, description, buyButtonOption, progressBarOption);
} }
private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel) private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel)
@@ -1197,31 +1240,21 @@ namespace Barotrauma
Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub;
if (Campaign is null || sub is null) { return; } if (Campaign is null || sub is null) { return; }
GUIFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.25f, parent)); UpgradeFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.4f, parent));
var prefabLayout = prefabFrame.GetChild<GUILayoutGroup>();
GUILayoutGroup[] childLayouts = prefabLayout.GetAllChildren<GUILayoutGroup>().ToArray(); if (!prefabFrame.BuyButton.TryUnwrap(out BuyButtonFrame buyButtonFrame)) { return; }
var imageLayout = childLayouts[0];
var icon = imageLayout.GetChild<GUIImage>();
var textLayout = childLayouts[1];
var name = textLayout.GetChild<GUITextBlock>();
GUILayoutGroup[] textChildLayouts = textLayout.GetAllChildren<GUILayoutGroup>().ToArray();
var descriptionLayout = textChildLayouts[0];
var description = descriptionLayout.GetChild<GUITextBlock>();
var progressLayout = textChildLayouts[1];
var buyButtonLayout = childLayouts[2];
var buyButton = buyButtonLayout.GetChild<GUIButton>();
if (!HasPermission || !prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab)))) if (!HasPermission || !prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab))))
{ {
prefabFrame.Enabled = false; prefabFrame.Frame.Enabled = false;
description.Enabled = false; prefabFrame.Description.Enabled = false;
name.Enabled = false; prefabFrame.Name.Enabled = false;
icon.Color = Color.Gray; prefabFrame.Icon.Color = Color.Gray;
buyButton.Enabled = false; buyButtonFrame.BuyButton.Enabled = false;
buyButtonLayout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button buyButtonFrame.Layout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button
} }
buyButton.OnClicked += (button, o) => buyButtonFrame.BuyButton.OnClicked += (button, o) =>
{ {
LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody",
("[upgradename]", prefab.Name), ("[upgradename]", prefab.Name),
@@ -1240,7 +1273,7 @@ namespace Barotrauma
return true; return true;
}; };
UpdateUpgradeEntry(prefabFrame, prefab, category, Campaign); UpdateUpgradeEntry(prefabFrame.Frame, prefab, category, Campaign);
} }
private void CreateItemTooltip(MapEntity entity) private void CreateItemTooltip(MapEntity entity)
@@ -1623,7 +1656,7 @@ namespace Barotrauma
int maxLevel = prefab.GetMaxLevelForCurrentSub(); int maxLevel = prefab.GetMaxLevelForCurrentSub();
LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", maxLevel.ToString())); LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", maxLevel.ToString()));
if (prefabFrame.FindChild("progressbar", true) is { } progressParent) if (prefabFrame.FindChild(UpgradeStoreUserData.ProgressBarLayout, true) is { } progressParent)
{ {
GUIProgressBar bar = progressParent.GetChild<GUIProgressBar>(); GUIProgressBar bar = progressParent.GetChild<GUIProgressBar>();
if (bar != null) if (bar != null)
@@ -1636,36 +1669,111 @@ namespace Barotrauma
if (block != null) { block.Text = progressText; } if (block != null) { block.Text = progressText; }
} }
if (prefabFrame.FindChild("buybutton", true) is { } buttonParent) if (prefabFrame.FindChild(UpgradeStoreUserData.BuyButtonLayout, true) is not { } buttonParent) { return; }
GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true);
priceLabel.Visible = true;
int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
if (!WaitForServerUpdate)
{ {
List<GUITextBlock> textBlocks = buttonParent.GetAllChildren<GUITextBlock>().ToList(); priceLabel.Text = TextManager.FormatCurrency(price);
if (currentLevel >= maxLevel)
GUITextBlock priceLabel = textBlocks[0];
priceLabel.Visible = true;
int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
if (priceLabel != null && !WaitForServerUpdate)
{ {
priceLabel.Text = TextManager.FormatCurrency(price); priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade");
if (currentLevel >= maxLevel)
{
priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade");
}
} }
}
GUIButton button = buttonParent.GetChild<GUIButton>(); if (buttonParent.FindChild(UpgradeStoreUserData.IncreaseLabel, recursive: true) is GUITextBlock increaseLabel && !WaitForServerUpdate)
if (button != null) {
UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel);
}
bool isMax = currentLevel >= maxLevel;
if (buttonParent.FindChild(UpgradeStoreUserData.BuyButton, recursive: true) is GUIButton button)
{
bool canBuy = !WaitForServerUpdate && !isMax && campaign.GetBalance() >= price && prefab.HasResourcesToUpgrade(Character.Controlled, currentLevel + 1);
button.Enabled = canBuy;
}
if (prefabFrame.FindChild(UpgradeStoreUserData.MaterialCostList, true) is GUIListBox itemList)
{
if (isMax)
{ {
button.Enabled = currentLevel < maxLevel; itemList.Visible = false;
if (WaitForServerUpdate || campaign.GetBalance() < price)
{
button.Enabled = false;
}
} }
GUITextBlock increaseLabel = textBlocks[1]; else
if (increaseLabel != null && !WaitForServerUpdate)
{ {
UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel); CreateMaterialCosts(itemList, prefab, currentLevel + 1);
}
}
static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel)
{
list.Content.ClearChildren();
List<Item> allItems = Character.Controlled?.Inventory?.FindAllItems(recursive: true) ?? new List<Item>();
var resources = prefab.GetApplicableResources(targetLevel);
foreach (ApplicableResourceCollection collection in resources)
{
list.Visible = true;
int length = collection.MatchingItems.Length;
if (length is 0) { continue; }
ItemPrefab defaultItemPrefab = collection.MatchingItems.First();
GUILayoutGroup wrapperLayout = new GUILayoutGroup(rectT(0.25f, 1f, list.Content));
GUIFrame itemFrame = new GUIFrame(rectT(1f, 1f, wrapperLayout), style: null)
{
ToolTip = defaultItemPrefab.Name
};
bool hasItems = collection.Cost.Amount <= allItems.Count(collection.Cost.MatchesItem);
Sprite icon = defaultItemPrefab.InventoryIcon ?? prefab.Sprite;
Color iconColor = defaultItemPrefab.InventoryIcon is null ? defaultItemPrefab.SpriteColor : defaultItemPrefab.InventoryIconColor;
GUIImage itemIcon = new GUIImage(new RectTransform(Vector2.One, itemFrame.RectTransform, scaleBasis: ScaleBasis.Smallest, anchor: Anchor.Center), sprite: icon, scaleToFit: true)
{
Color = hasItems ? iconColor : iconColor * 0.9f,
CanBeFocused = false
};
// item count text
new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), itemIcon.RectTransform, anchor: Anchor.BottomRight), $"{collection.Count}", font: GUIStyle.Font, textAlignment: Alignment.BottomRight)
{
Shadow = true,
CanBeFocused = false,
Padding = Vector4.Zero,
TextColor = hasItems ? Color.White : GUIStyle.Red,
};
if (length is 1) { continue; }
// we have more than 1 item, show a "slideshow" of the items
float index = 0f;
GUICustomComponent customComponent = new GUICustomComponent(rectT(1f, 1f, itemFrame), null, (deltaTime, component) =>
{
index += deltaTime / 3f;
if (index > length) { index = 0; }
ItemPrefab currentPrefab = collection.MatchingItems[(int)MathF.Floor(index)];
Sprite icon = currentPrefab.InventoryIcon ?? prefab.Sprite;
Color iconColor = currentPrefab.InventoryIcon is null ? currentPrefab.SpriteColor : currentPrefab.InventoryIconColor;
itemIcon.Sprite = icon;
itemIcon.Color = hasItems ? iconColor : iconColor * 0.9f;
itemFrame.ToolTip = currentPrefab.Name;
})
{
CanBeFocused = false
};
} }
} }
} }
@@ -17,6 +17,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using System.Collections.Immutable;
namespace Barotrauma namespace Barotrauma
{ {
@@ -475,6 +476,19 @@ namespace Barotrauma
yield return CoroutineStatus.Running; yield return CoroutineStatus.Running;
} }
var corePackage = ContentPackageManager.EnabledPackages.Core;
if (corePackage.EnableError.TryUnwrap(out var error))
{
if (error.ErrorsOrException.TryGet(out ImmutableArray<string> errorMessages))
{
throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {errorMessages.First()}");
}
else if (error.ErrorsOrException.TryGet(out Exception exception))
{
throw new Exception($"Error while loading the core content package \"{corePackage.Name}\": {exception.Message}", exception);
}
}
TextManager.VerifyLanguageAvailable(); TextManager.VerifyLanguageAvailable();
DebugConsole.Init(); DebugConsole.Init();
@@ -735,8 +749,8 @@ namespace Barotrauma
{ {
Client.Quit(); Client.Quit();
Client = null; Client = null;
MainMenuScreen.Select();
} }
MainMenuScreen.Select();
if (connectCommand.EndpointOrLobby.TryGet(out ulong lobbyId)) if (connectCommand.EndpointOrLobby.TryGet(out ulong lobbyId))
{ {
@@ -1099,37 +1113,6 @@ namespace Barotrauma
GameSession = null; GameSession = null;
} }
public void ShowEditorDisclaimer()
{
var msgBox = new GUIMessageBox(TextManager.Get("EditorDisclaimerTitle"), TextManager.Get("EditorDisclaimerText"));
var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f };
linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height);
List<(LocalizedString Caption, string Url)> links = new List<(LocalizedString, string)>()
{
(TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value),
(TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl").Fallback("https://discordapp.com/invite/undertow").Value),
};
foreach (var link in links)
{
new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), linkHolder.RectTransform), link.Caption, style: "MainMenuGUIButton", textAlignment: Alignment.Left)
{
UserData = link.Url,
OnClicked = (btn, userdata) =>
{
ShowOpenUrlInWebBrowserPrompt(userdata as string);
return true;
}
};
}
msgBox.InnerFrame.RectTransform.MinSize = new Point(0,
msgBox.InnerFrame.Rect.Height + linkHolder.Rect.Height + msgBox.Content.AbsoluteSpacing * 2 + 10);
var config = GameSettings.CurrentConfig;
config.EditorDisclaimerShown = true;
GameSettings.SetCurrentConfig(config);
GameSettings.SaveCurrentConfig();
}
public void ShowBugReporter() public void ShowBugReporter()
{ {
if (GUIMessageBox.VisibleBox != null && GUIMessageBox.VisibleBox.UserData as string == "bugreporter") if (GUIMessageBox.VisibleBox != null && GUIMessageBox.VisibleBox.UserData as string == "bugreporter")
@@ -106,12 +106,7 @@ namespace Barotrauma
public static bool AllowedToManageWallets() public static bool AllowedToManageWallets()
{ {
if (GameMain.Client == null) { return true; } return AllowedToManageCampaign(ClientPermissions.ManageMoney);
return
GameMain.Client.HasPermission(ClientPermissions.ManageMoney) ||
GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Client.IsServerOwner;
} }
public override void Draw(SpriteBatch spriteBatch) public override void Draw(SpriteBatch spriteBatch)
@@ -965,7 +965,7 @@ namespace Barotrauma
break; break;
case QuickUseAction.PutToEquippedItem: case QuickUseAction.PutToEquippedItem:
//order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank //order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank
foreach (Item heldItem in character.HeldItems.OrderBy(it => it.ContainedItems.FirstOrDefault()?.Condition ?? 0.0f)) foreach (Item heldItem in character.HeldItems.OrderBy(it => it.GetComponent<ItemContainer>()?.GetContainedIndicatorState() ?? 0.0f))
{ {
if (heldItem.OwnInventory == null) { continue; } if (heldItem.OwnInventory == null) { continue; }
//don't allow swapping if we're moving items into an item with 1 slot holding a stack of items //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items
@@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components
public void DrawElectricity(SpriteBatch spriteBatch) public void DrawElectricity(SpriteBatch spriteBatch)
{ {
if (timer <= 0.0f) { return; } if (timer <= 0.0f && Screen.Selected is { IsEditor: false }) { return; }
for (int i = 0; i < nodes.Count; i++) for (int i = 0; i < nodes.Count; i++)
{ {
if (nodes[i].Length <= 1.0f) { continue; } if (nodes[i].Length <= 1.0f) { continue; }
@@ -1,7 +1,9 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections;
using System.Linq; using System.Linq;
using static Barotrauma.Inventory;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
@@ -250,6 +252,83 @@ namespace Barotrauma.Items.Components
return true; return true;
} }
public float GetContainedIndicatorState()
{
if (ShowConditionInContainedStateIndicator)
{
return item.Condition / item.MaxCondition;
}
int targetSlot = Math.Max(ContainedStateIndicatorSlot, 0);
if (targetSlot >= Inventory.Capacity) { return 0.0f; }
var containedItems = Inventory.GetItemsAt(targetSlot);
if (containedItems == null) { return 0.0f; }
Item containedItem = containedItems.FirstOrDefault();
if (ShowTotalStackCapacityInContainedStateIndicator)
{
// No item on the defined slot, check if the items on other slots can be used.
containedItem ??=
containedItems.FirstOrDefault() ??
Inventory.AllItems.FirstOrDefault(it => CanBeContained(it, targetSlot));
if (containedItem == null) { return 0.0f; }
int ignoredItemCount = 0;
var subContainableItems = AllSubContainableItems;
float capacity = GetMaxStackSize(targetSlot);
if (subContainableItems != null)
{
bool useMainContainerCapacity = true;
foreach (Item it in Inventory.AllItems)
{
// Ignore all items in the sub containers.
foreach (RelatedItem ri in subContainableItems)
{
if (ri.MatchesItem(containedItem))
{
// The target item is in a subcontainer -> inverse the logic.
useMainContainerCapacity = false;
break;
}
if (ri.MatchesItem(it))
{
ignoredItemCount++;
}
}
if (!useMainContainerCapacity) { break; }
}
if (useMainContainerCapacity)
{
capacity *= MainContainerCapacity;
}
else
{
// Ignore all items in the main container.
ignoredItemCount = Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it)));
capacity *= Capacity - MainContainerCapacity;
}
}
int itemCount = Inventory.AllItems.Count() - ignoredItemCount;
return Math.Min(itemCount / Math.Max(capacity, 1), 1);
}
else
{
if (containedItem != null && (Inventory.Capacity == 1 || HasSubContainers))
{
int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, GetMaxStackSize(targetSlot));
if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar)
{
return containedItems.Count() / (float)maxStackSize;
}
}
return Inventory.Capacity == 1 || ContainedStateIndicatorSlot > -1 ?
(containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) :
Inventory.EmptySlotCount / (float)Inventory.Capacity;
}
}
public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1)
{ {
if (hideItems || (item.body != null && !item.body.Enabled)) { return; } if (hideItems || (item.body != null && !item.body.Enabled)) { return; }
@@ -14,8 +14,6 @@ namespace Barotrauma.Items.Components
private CoroutineHandle resetPredictionCoroutine; private CoroutineHandle resetPredictionCoroutine;
private float resetPredictionTimer; private float resetPredictionTimer;
private float currentBrightness;
public Vector2 DrawSize public Vector2 DrawSize
{ {
get { return new Vector2(Light.Range * 2, Light.Range * 2); } get { return new Vector2(Light.Range * 2, Light.Range * 2); }
@@ -29,14 +27,21 @@ namespace Barotrauma.Items.Components
Light.Position = ParentBody != null ? ParentBody.Position : item.Position; Light.Position = ParentBody != null ? ParentBody.Position : item.Position;
} }
partial void SetLightSourceState(bool enabled, float brightness) partial void SetLightSourceState(bool enabled, float? brightness)
{ {
if (Light == null) { return; } if (Light == null) { return; }
Light.Enabled = enabled; Light.Enabled = enabled;
currentBrightness = brightness; if (brightness.HasValue)
{
lightBrightness = brightness.Value;
}
else
{
lightBrightness = enabled ? 1.0f : 0.0f;
}
if (enabled) if (enabled)
{ {
Light.Color = LightColor.Multiply(brightness); Light.Color = LightColor.Multiply(lightBrightness);
} }
} }
@@ -519,7 +519,10 @@ namespace Barotrauma.Items.Components
Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green; Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green;
weaponSprite.Draw(batch, center, color, origin, rotation, scale, SpriteEffects.None); weaponSprite.Draw(batch, center, color, origin, rotation, scale, SpriteEffects.None);
} }
}); })
{
CanBeFocused = false
};
weaponChilds.Add(component, frame); weaponChilds.Add(component, frame);
} }
@@ -25,14 +25,14 @@ namespace Barotrauma.Items.Components
public static Color editorHighlightColor = Color.Yellow; public static Color editorHighlightColor = Color.Yellow;
public static Color editorSelectedColor = Color.Red; public static Color editorSelectedColor = Color.Red;
partial class WireSection public partial class WireSection
{ {
public VertexPositionColorTexture[] vertices; public VertexPositionColorTexture[] vertices;
public VertexPositionColorTexture[] shiftedVertices; public VertexPositionColorTexture[] shiftedVertices;
private float cachedWidth = 0f; private float cachedWidth = 0f;
private void RecalculateVertices(Wire wire, float width) private void RecalculateVertices(Sprite wireSprite, float width)
{ {
if (MathUtils.NearlyEqual(cachedWidth, width)) { return; } if (MathUtils.NearlyEqual(cachedWidth, width)) { return; }
cachedWidth = width; cachedWidth = width;
@@ -45,13 +45,13 @@ namespace Barotrauma.Items.Components
expandDir.X = -expandDir.Y; expandDir.X = -expandDir.Y;
expandDir.Y = -temp; expandDir.Y = -temp;
Rectangle srcRect = wire.wireSprite.SourceRect; Rectangle srcRect = wireSprite.SourceRect;
expandDir *= width * srcRect.Height * 0.5f; expandDir *= width * srcRect.Height * 0.5f;
Vector2 rectLocation = srcRect.Location.ToVector2(); Vector2 rectLocation = srcRect.Location.ToVector2();
Vector2 rectSize = srcRect.Size.ToVector2(); Vector2 rectSize = srcRect.Size.ToVector2();
Vector2 textureSize = new Vector2(wire.wireSprite.Texture.Width, wire.wireSprite.Texture.Height); Vector2 textureSize = new Vector2(wireSprite.Texture.Width, wireSprite.Texture.Height);
Vector2 topLeftUv = rectLocation / textureSize; Vector2 topLeftUv = rectLocation / textureSize;
Vector2 bottomRightUv = (rectLocation + rectSize) / textureSize; Vector2 bottomRightUv = (rectLocation + rectSize) / textureSize;
@@ -67,10 +67,10 @@ namespace Barotrauma.Items.Components
shiftedVertices = (VertexPositionColorTexture[])vertices.Clone(); shiftedVertices = (VertexPositionColorTexture[])vertices.Clone();
} }
public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) public void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Color color, Vector2 offset, float depth, float width = 0.3f)
{ {
if (width <= 0f) { return; } if (width <= 0f) { return; }
RecalculateVertices(wire, width); RecalculateVertices(wireSprite, width);
for (int i = 0; i < vertices.Length; i++) for (int i = 0; i < vertices.Length; i++)
{ {
@@ -79,21 +79,22 @@ namespace Barotrauma.Items.Components
shiftedVertices[i].Position.X += offset.X; shiftedVertices[i].Position.X += offset.X;
shiftedVertices[i].Position.Y -= offset.Y; shiftedVertices[i].Position.Y -= offset.Y;
} }
spriteBatch.Draw(wire.wireSprite.Texture, spriteBatch.Draw(
wireSprite.Texture,
shiftedVertices, shiftedVertices,
depth); depth);
} }
public static void Draw(SpriteBatch spriteBatch, Wire wire, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) public static void Draw(ISpriteBatch spriteBatch, Sprite wireSprite, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f)
{ {
start.Y = -start.Y; start.Y = -start.Y;
end.Y = -end.Y; end.Y = -end.Y;
spriteBatch.Draw(wire.wireSprite.Texture, spriteBatch.Draw(wireSprite.Texture,
start, wire.wireSprite.SourceRect, color, start, wireSprite.SourceRect, color,
MathUtils.VectorToAngle(end - start), MathUtils.VectorToAngle(end - start),
new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), new Vector2(0.0f, wireSprite.size.Y / 2.0f),
new Vector2((Vector2.Distance(start, end)) / wire.wireSprite.size.X, width), new Vector2((Vector2.Distance(start, end)) / wireSprite.size.X, width),
SpriteEffects.None, SpriteEffects.None,
depth); depth);
} }
@@ -123,7 +124,7 @@ namespace Barotrauma.Items.Components
get => draggingWire; get => draggingWire;
} }
partial void InitProjSpecific(ContentXElement element) public static Sprite ExtractWireSprite(ContentXElement element)
{ {
if (defaultWireSprite == null) if (defaultWireSprite == null)
{ {
@@ -133,6 +134,7 @@ namespace Barotrauma.Items.Components
}; };
} }
Sprite overrideSprite = null;
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase)) if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase))
@@ -142,9 +144,14 @@ namespace Barotrauma.Items.Components
} }
} }
wireSprite = overrideSprite ?? defaultWireSprite; return overrideSprite ?? defaultWireSprite;
}
partial void InitProjSpecific(ContentXElement element)
{
wireSprite = ExtractWireSprite(element);
if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; }
} }
public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1)
{ {
@@ -181,20 +188,20 @@ namespace Barotrauma.Items.Components
{ {
foreach (WireSection section in sections) foreach (WireSection section in sections)
{ {
section.Draw(spriteBatch, this, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); section.Draw(spriteBatch, wireSprite, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f);
} }
} }
else if (item.IsSelected) else if (item.IsSelected)
{ {
foreach (WireSection section in sections) foreach (WireSection section in sections)
{ {
section.Draw(spriteBatch, this, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f); section.Draw(spriteBatch, wireSprite, editorSelectedColor, drawOffset, depth + 0.00001f, Width * 2.0f);
} }
} }
foreach (WireSection section in sections) foreach (WireSection section in sections)
{ {
section.Draw(spriteBatch, this, item.Color, drawOffset, depth, Width); section.Draw(spriteBatch, wireSprite, item.Color, drawOffset, depth, Width);
} }
if (nodes.Count > 0) if (nodes.Count > 0)
@@ -239,13 +246,13 @@ namespace Barotrauma.Items.Components
} }
WireSection.Draw( WireSection.Draw(
spriteBatch, this, spriteBatch, wireSprite,
new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, nodes[^1] + drawOffset,
new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset,
item.Color, 0.0f, Width); item.Color, 0.0f, Width);
WireSection.Draw( WireSection.Draw(
spriteBatch, this, spriteBatch, wireSprite,
new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset,
item.DrawPosition, item.DrawPosition,
item.Color, itemDepth, Width); item.Color, itemDepth, Width);
@@ -255,8 +262,8 @@ namespace Barotrauma.Items.Components
else else
{ {
WireSection.Draw( WireSection.Draw(
spriteBatch, this, spriteBatch, wireSprite,
new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, nodes[^1] + drawOffset,
item.DrawPosition, item.DrawPosition,
item.Color, 0.0f, Width); item.Color, 0.0f, Width);
} }
@@ -294,12 +301,12 @@ namespace Barotrauma.Items.Components
Vector2 endPos = start + new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)) * 50.0f; Vector2 endPos = start + new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)) * 50.0f;
WireSection.Draw( WireSection.Draw(
spriteBatch, this, spriteBatch, wireSprite,
start, endPos, start, endPos,
GUIStyle.Orange, depth + 0.00001f, 0.2f); GUIStyle.Orange, depth + 0.00001f, 0.2f);
WireSection.Draw( WireSection.Draw(
spriteBatch, this, spriteBatch, wireSprite,
start, start + (endPos - start) * 0.7f, start, start + (endPos - start) * 0.7f,
item.Color, depth, 0.3f); item.Color, depth, 0.3f);
} }
@@ -1603,88 +1603,7 @@ namespace Barotrauma
if (itemContainer != null && itemContainer.ShowContainedStateIndicator && itemContainer.Capacity > 0) if (itemContainer != null && itemContainer.ShowContainedStateIndicator && itemContainer.Capacity > 0)
{ {
float containedState = 0.0f; float containedState = itemContainer.GetContainedIndicatorState();
if (itemContainer.ShowConditionInContainedStateIndicator)
{
containedState = item.Condition / item.MaxCondition;
}
else
{
int targetSlot = Math.Max(itemContainer.ContainedStateIndicatorSlot, 0);
ItemSlot containedItemSlot = null;
if (targetSlot < itemContainer.Inventory.slots.Length)
{
containedItemSlot = itemContainer.Inventory.slots[targetSlot];
}
if (containedItemSlot != null)
{
Item containedItem = containedItemSlot.FirstOrDefault();
if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator)
{
if (containedItem == null)
{
// No item on the defined slot, check if the items on other slots can be used.
containedItem = containedItemSlot.FirstOrDefault() ?? itemContainer.Inventory.AllItems.FirstOrDefault(it => itemContainer.CanBeContained(it, targetSlot));
}
if (containedItem != null)
{
int ignoredItemCount = 0;
var subContainableItems = itemContainer.AllSubContainableItems;
float capacity = itemContainer.GetMaxStackSize(targetSlot);
if (subContainableItems != null)
{
bool useMainContainerCapacity = true;
foreach (Item it in itemContainer.Inventory.AllItems)
{
// Ignore all items in the sub containers.
foreach (RelatedItem ri in subContainableItems)
{
if (ri.MatchesItem(containedItem))
{
// The target item is in a subcontainer -> inverse the logic.
useMainContainerCapacity = false;
break;
}
if (ri.MatchesItem(it))
{
ignoredItemCount++;
}
}
if (!useMainContainerCapacity) { break; }
}
if (useMainContainerCapacity)
{
capacity *= itemContainer.MainContainerCapacity;
}
else
{
// Ignore all items in the main container.
ignoredItemCount = itemContainer.Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it)));
capacity *= itemContainer.Capacity - itemContainer.MainContainerCapacity;
}
}
int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItemCount;
containedState = Math.Min(itemCount / Math.Max(capacity, 1), 1);
}
}
else
{
containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ?
(containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) :
itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity;
if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers))
{
int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot));
if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar)
{
containedState = containedItemSlot.Items.Count / (float)maxStackSize;
}
}
}
}
}
int dir = slot.SubInventoryDir; int dir = slot.SubInventoryDir;
Rectangle containedIndicatorArea = new Rectangle(rect.X, Rectangle containedIndicatorArea = new Rectangle(rect.X,
dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight); dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight);
@@ -1807,6 +1726,7 @@ namespace Barotrauma
} }
} }
private static void DrawItemStateIndicator( private static void DrawItemStateIndicator(
SpriteBatch spriteBatch, Inventory inventory, SpriteBatch spriteBatch, Inventory inventory,
Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState, Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState,
@@ -260,16 +260,16 @@ namespace Barotrauma
public override void UpdatePlacing(Camera cam) public override void UpdatePlacing(Camera cam)
{ {
Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub);
if (PlayerInput.SecondaryMouseButtonClicked()) if (PlayerInput.SecondaryMouseButtonClicked())
{ {
Selected = null; Selected = null;
return; return;
} }
var potentialContainer = MapEntity.GetPotentialContainer(cam.ScreenToWorld(PlayerInput.MousePosition));
var potentialContainer = MapEntity.GetPotentialContainer(position); Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub);
if (!ResizeHorizontal && !ResizeVertical) if (!ResizeHorizontal && !ResizeVertical)
{ {
if (PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) if (PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null)
@@ -293,11 +293,11 @@ namespace Barotrauma
GUI.DrawRectangle(spriteBatch, GUI.DrawRectangle(spriteBatch,
new Vector2(drawRect.X, -drawRect.Y), new Vector2(drawRect.X, -drawRect.Y),
new Vector2(rect.Width, rect.Height), new Vector2(rect.Width, rect.Height),
Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(MathF.Ceiling(1.5f / Screen.Selected.Cam.Zoom), 1.0f));
GUI.DrawRectangle(spriteBatch, GUI.DrawRectangle(spriteBatch,
new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height),
GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(MathF.Ceiling(1.5f / Screen.Selected.Cam.Zoom), 1.0f));
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
{ {
@@ -515,11 +515,11 @@ namespace Barotrauma
Item targetContainer = null; Item targetContainer = null;
bool isShiftDown = PlayerInput.IsShiftDown(); bool isShiftDown = PlayerInput.IsShiftDown();
if (!isShiftDown) return null; if (!isShiftDown) { return null; }
foreach (MapEntity e in mapEntityList) foreach (MapEntity e in mapEntityList)
{ {
if (!e.SelectableInEditor ||!(e is Item potentialContainer)) { continue; } if (!e.SelectableInEditor || e is not Item potentialContainer) { continue; }
if (e.IsMouseOn(position)) if (e.IsMouseOn(position))
{ {
@@ -83,7 +83,10 @@ namespace Barotrauma
{ {
string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found)."; string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found).";
DebugConsole.ThrowError(errorMsg, e); DebugConsole.ThrowError(errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); if (!ContentPackageManager.ModsEnabled)
{
GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
}
return null; return null;
} }
catch (System.IO.InvalidDataException e) catch (System.IO.InvalidDataException e)
@@ -4,26 +4,29 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma namespace Barotrauma
{ {
class SubmarinePreview : IDisposable sealed class SubmarinePreview : IDisposable
{ {
private SpriteRecorder spriteRecorder;
private readonly SubmarineInfo submarineInfo; private readonly SubmarineInfo submarineInfo;
private SpriteRecorder spriteRecorder;
private Camera camera; private Camera camera;
private Task loadTask; private Task loadTask;
private (Vector2 Min, Vector2 Max) bounds;
private volatile bool isDisposed; private volatile bool isDisposed;
private GUIFrame previewFrame; private GUIFrame previewFrame;
private class HullCollection private sealed class HullCollection
{ {
public readonly List<Rectangle> Rects; public readonly List<Rectangle> Rects;
public readonly LocalizedString Name; public readonly LocalizedString Name;
@@ -186,7 +189,21 @@ namespace Barotrauma
}); });
recalculateSpecsContainerHeight(); recalculateSpecsContainerHeight();
GeneratePreviewMeshes(); TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ =>
{
// Reset the camera's position on the main thread,
// because the Camera class is not thread-safe and
// it's possible for its state to not get updated
// properly if done within a task
camera.Position = (bounds.Min + bounds.Max) * (0.5f, -0.5f);
Vector2 span2d = bounds.Max - bounds.Min;
Vector2 scaledSpan2d = span2d / camera.Resolution.ToVector2();
float scaledSpan = Math.Max(scaledSpan2d.X, scaledSpan2d.Y);
camera.MinZoom = Math.Min(0.1f, 0.4f / scaledSpan);
camera.Zoom = 0.7f / scaledSpan;
camera.StopMovement();
camera.UpdateTransform(interpolate: false, updateListener: false);
});
} }
public static void AddToGUIUpdateList() public static void AddToGUIUpdateList()
@@ -207,6 +224,7 @@ namespace Barotrauma
spriteRecorder.Begin(SpriteSortMode.BackToFront); spriteRecorder.Begin(SpriteSortMode.BackToFront);
HashSet<int> toIgnore = new HashSet<int>(); HashSet<int> toIgnore = new HashSet<int>();
HashSet<int> wires = new HashSet<int>();
foreach (var subElement in submarineInfo.SubmarineElement.Elements()) foreach (var subElement in submarineInfo.SubmarineElement.Elements())
{ {
@@ -221,7 +239,7 @@ namespace Barotrauma
ExtractItemContainerIds(component, toIgnore); ExtractItemContainerIds(component, toIgnore);
break; break;
case "connectionpanel": case "connectionpanel":
ExtractConnectionPanelLinks(component, toIgnore); ExtractConnectionPanelLinks(component, wires);
break; break;
} }
} }
@@ -231,20 +249,25 @@ namespace Barotrauma
await Task.Yield(); await Task.Yield();
} }
var wireNodes = new List<XElement>();
foreach (var subElement in submarineInfo.SubmarineElement.Elements()) foreach (var subElement in submarineInfo.SubmarineElement.Elements())
{ {
if (subElement.GetAttributeBool("hiddeningame", false)) { continue; } if (subElement.GetAttributeBool("hiddeningame", false)) { continue; }
switch (subElement.Name.LocalName.ToLowerInvariant()) switch (subElement.Name.LocalName.ToLowerInvariant())
{ {
case "structure":
case "item": case "item":
if (!toIgnore.Contains(subElement.GetAttributeInt("ID", 0))) var id = subElement.GetAttributeInt("ID", 0);
if (wires.Contains(id))
{
wireNodes.Add(subElement);
}
else if (!toIgnore.Contains(id))
{ {
BakeMapEntity(subElement); BakeMapEntity(subElement);
} }
break; break;
case "structure":
BakeMapEntity(subElement);
break;
case "hull": case "hull":
Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); Identifier identifier = subElement.GetAttributeIdentifier("roomname", "");
if (!identifier.IsEmpty) if (!identifier.IsEmpty)
@@ -261,15 +284,14 @@ namespace Barotrauma
if (isDisposed) { return; } if (isDisposed) { return; }
await Task.Yield(); await Task.Yield();
} }
spriteRecorder.End();
camera.Position = (spriteRecorder.Min + spriteRecorder.Max) * 0.5f; bounds = (spriteRecorder.Min, spriteRecorder.Max);
float scaledSpan = (spriteRecorder.Max - spriteRecorder.Min).X / camera.Resolution.X; wireNodes.ForEach(BakeWireNodes);
camera.Zoom = 0.8f / scaledSpan;
camera.StopMovement(); spriteRecorder.End();
} }
private void ExtractItemContainerIds(XElement component, HashSet<int> ids) private static void ExtractItemContainerIds(XElement component, HashSet<int> ids)
{ {
string containedString = component.GetAttributeString("contained", ""); string containedString = component.GetAttributeString("contained", "");
string[] itemIdStrings = containedString.Split(','); string[] itemIdStrings = containedString.Split(',');
@@ -283,7 +305,7 @@ namespace Barotrauma
} }
} }
private void ExtractConnectionPanelLinks(XElement component, HashSet<int> ids) private static void ExtractConnectionPanelLinks(XElement component, HashSet<int> ids)
{ {
var pins = component.Elements("input").Concat(component.Elements("output")); var pins = component.Elements("input").Concat(component.Elements("output"));
foreach (var pin in pins) foreach (var pin in pins)
@@ -297,6 +319,39 @@ namespace Barotrauma
} }
} }
private void BakeWireNodes(XElement element)
{
var prefabIdentifier = element.GetAttributeIdentifier("identifier", "");
if (prefabIdentifier.IsEmpty) { return; }
if (!ItemPrefab.Prefabs.TryGet(prefabIdentifier, out var prefab)) { return; }
var prefabWireComponentElement = prefab.ConfigElement.GetChildElement("wire");
if (prefabWireComponentElement is null) { return; }
var wireComponent = element.GetChildElement("wire");
if (wireComponent is null) { return; }
var color = element.GetAttributeColor("spritecolor") ?? Color.White;
var nodes = Wire.ExtractNodes(wireComponent).ToImmutableArray();
var wireSprite = Wire.ExtractWireSprite(prefab.ConfigElement);
var useSpriteDepth = element.GetAttributeBool("usespritedepth", false);
var depth =
useSpriteDepth
? element.GetAttributeFloat("spritedepth", 1.0f)
: wireSprite.Depth;
var width = prefabWireComponentElement.GetAttributeFloat("width", 0.3f);
for (int i = 0; i < nodes.Length - 1; i++)
{
var line = (Start: nodes[i], End: nodes[i + 1]);
var wireSegment = new Wire.WireSection(line.Start, line.End);
wireSegment.Draw(spriteRecorder, wireSprite, color, Vector2.Zero, depth, width);
}
}
private void BakeMapEntity(XElement element) private void BakeMapEntity(XElement element)
{ {
Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
@@ -313,27 +368,27 @@ namespace Barotrauma
float rotation = element.GetAttributeFloat("rotation", 0f); float rotation = element.GetAttributeFloat("rotation", 0f);
MapEntityPrefab prefab = null; MapEntityPrefab prefab;
if (element.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase) && if (element.NameAsIdentifier() == "item"
ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) && ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip))
{ {
prefab = ip; prefab = ip;
} }
else else
{ {
prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); prefab = MapEntityPrefab.FindByIdentifier(identifier);
} }
if (prefab == null) { return; } if (prefab == null) { return; }
var texture = prefab.Sprite.Texture; flippedX &= prefab.CanSpriteFlipX;
var srcRect = prefab.Sprite.SourceRect; flippedY &= prefab.CanSpriteFlipY;
SpriteEffects spriteEffects = SpriteEffects.None; SpriteEffects spriteEffects = SpriteEffects.None;
if (flippedX && ((prefab as ItemPrefab)?.CanSpriteFlipX ?? true)) if (flippedX)
{ {
spriteEffects |= SpriteEffects.FlipHorizontally; spriteEffects |= SpriteEffects.FlipHorizontally;
} }
if (flippedY && ((prefab as ItemPrefab)?.CanSpriteFlipY ?? true)) if (flippedY)
{ {
spriteEffects |= SpriteEffects.FlipVertically; spriteEffects |= SpriteEffects.FlipVertically;
} }
@@ -419,8 +474,8 @@ namespace Barotrauma
{ {
float offsetState = 0f; float offsetState = 0f;
Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale;
if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedX) { offset.X = -offset.X; }
if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } if (flippedY) { offset.Y = -offset.Y; }
decorativeSprite.Sprite.DrawTiled(spriteRecorder, decorativeSprite.Sprite.DrawTiled(spriteRecorder,
new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)),
rect.Size.ToVector2(), color: color, rect.Size.ToVector2(), color: color,
@@ -451,8 +506,8 @@ namespace Barotrauma
float rotationState = 0f; float offsetState = 0f; float rotationState = 0f; float offsetState = 0f;
float rot = decorativeSprite.GetRotation(ref rotationState, 0f); float rot = decorativeSprite.GetRotation(ref rotationState, 0f);
Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale;
if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedX) { offset.X = -offset.X; }
if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } if (flippedY) { offset.Y = -offset.Y; }
decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color,
MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects,
depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f));
@@ -472,6 +527,7 @@ namespace Barotrauma
{ {
overrideSprite = false; overrideSprite = false;
float relativeScale = scale / prefab.Scale;
foreach (var subElement in prefab.ConfigElement.Elements()) foreach (var subElement in prefab.ConfigElement.Elements())
{ {
switch (subElement.Name.LocalName.ToLowerInvariant()) switch (subElement.Name.LocalName.ToLowerInvariant())
@@ -498,7 +554,6 @@ namespace Barotrauma
relativeBarrelPos, relativeBarrelPos,
MathHelper.ToRadians(rotation)); MathHelper.ToRadians(rotation));
float relativeScale = scale / prefab.Scale;
Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale); Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale);
drawPos.Y = -drawPos.Y; drawPos.Y = -drawPos.Y;
@@ -516,20 +571,22 @@ namespace Barotrauma
break; break;
case "door": case "door":
doors.Add(new Door(rect)); var scaledRect = rect with { Size = (rect.Size.ToVector2() * relativeScale).ToPoint() };
doors.Add(new Door(scaledRect));
var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase)); var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase));
if (doorSpriteElem != null) if (doorSpriteElem != null)
{ {
string texturePath = doorSpriteElem.GetAttributeString("texture", ""); string texturePath = doorSpriteElem.GetAttributeStringUnrestricted("texture", "");
Vector2 pos = rect.Location.ToVector2() * new Vector2(1f, -1f); Vector2 pos = scaledRect.Location.ToVector2() * new Vector2(1f, -1f);
if (subElement.GetAttributeBool("horizontal", false)) if (subElement.GetAttributeBool("horizontal", false))
{ {
pos.Y += (float)rect.Height * 0.5f; pos.Y += (float)scaledRect.Height * 0.5f;
} }
else else
{ {
pos.X += (float)rect.Width * 0.5f; pos.X += (float)scaledRect.Width * 0.5f;
} }
Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath)); Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath));
spriteRecorder.Draw(doorSprite.Texture, pos, spriteRecorder.Draw(doorSprite.Texture, pos,
@@ -555,7 +612,7 @@ namespace Barotrauma
} }
} }
public void ParseUpgrades(XElement prefabConfigElement, ref float scale) private void ParseUpgrades(XElement prefabConfigElement, ref float scale)
{ {
foreach (var upgrade in prefabConfigElement.Elements("Upgrade")) foreach (var upgrade in prefabConfigElement.Elements("Upgrade"))
{ {
@@ -66,9 +66,20 @@ namespace Barotrauma.Networking
}; };
var addressOrAccountId = bannedPlayer.AddressOrAccountId; var addressOrAccountId = bannedPlayer.AddressOrAccountId;
GUITextBlock textBlock = new GUITextBlock(
new RectTransform(new Vector2(0.5f, 1.0f), topArea.RectTransform), string nameText = bannedPlayer.Name;
bannedPlayer.Name + " (" + addressOrAccountId + ")") { CanBeFocused = true }; if (addressOrAccountId.TryCast(out Address address))
{
nameText += $" ({address.StringRepresentation})";
}
else if (addressOrAccountId.TryCast(out AccountId accountId))
{
nameText += $" ({accountId.StringRepresentation})";
}
GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), topArea.RectTransform), nameText)
{
CanBeFocused = true
};
textBlock.RectTransform.MinSize = new Point( textBlock.RectTransform.MinSize = new Point(
(int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).X, 0); (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).X, 0);
@@ -106,7 +117,7 @@ namespace Barotrauma.Networking
private bool RemoveBan(GUIButton button, object obj) private bool RemoveBan(GUIButton button, object obj)
{ {
if (!(obj is BannedPlayer banned)) { return false; } if (obj is not BannedPlayer banned) { return false; }
localRemovedBans.Add(banned.UniqueIdentifier); localRemovedBans.Add(banned.UniqueIdentifier);
RecreateBanFrame(); RecreateBanFrame();
@@ -34,6 +34,7 @@ namespace Barotrauma.Networking
catch catch
{ {
DebugConsole.ThrowError($"Failed to start ChildServerRelay Process. File: {processInfo.FileName}, arguments: {processInfo.Arguments}"); DebugConsole.ThrowError($"Failed to start ChildServerRelay Process. File: {processInfo.FileName}, arguments: {processInfo.Arguments}");
ForceShutDown();
throw; throw;
} }
@@ -522,7 +522,7 @@ namespace Barotrauma.Networking
if (GameStarted && Screen.Selected == GameMain.GameScreen) if (GameStarted && Screen.Selected == GameMain.GameScreen)
{ {
EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned;
RespawnManager?.Update(deltaTime); RespawnManager?.Update(deltaTime);
@@ -1102,11 +1102,7 @@ namespace Barotrauma.Networking
VoipClient = new VoipClient(this, ClientPeer); VoipClient = new VoipClient(this, ClientPeer);
//if we're still in the game, roundsummary or lobby screen, we don't need to redownload the mods //if we're still in the game, roundsummary or lobby screen, we don't need to redownload the mods
if (!(Screen.Selected is GameScreen) && !(Screen.Selected is RoundSummaryScreen) && !(Screen.Selected is NetLobbyScreen)) if (Screen.Selected is GameScreen or RoundSummaryScreen or NetLobbyScreen)
{
GameMain.ModDownloadScreen.Select();
}
else
{ {
EntityEventManager.ClearSelf(); EntityEventManager.ClearSelf();
foreach (Character c in Character.CharacterList) foreach (Character c in Character.CharacterList)
@@ -1114,6 +1110,10 @@ namespace Barotrauma.Networking
c.ResetNetState(); c.ResetNetState();
} }
} }
else
{
GameMain.ModDownloadScreen.Select();
}
chatBox.InputBox.Enabled = true; chatBox.InputBox.Enabled = true;
if (GameMain.NetLobbyScreen?.ChatInput != null) if (GameMain.NetLobbyScreen?.ChatInput != null)
@@ -1535,8 +1535,9 @@ namespace Barotrauma.Networking
roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize; roundInitStatus = RoundInitStatus.WaitingForStartGameFinalize;
DateTime? timeOut = null; //wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message
TimeSpan timeOutDuration = new TimeSpan(0, 0, seconds: 30); TimeSpan timeOutDuration = new TimeSpan(0, 0, seconds: 30);
DateTime timeOut = DateTime.Now + timeOutDuration;
DateTime requestFinalizeTime = DateTime.Now; DateTime requestFinalizeTime = DateTime.Now;
TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2); TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2);
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
@@ -1545,11 +1546,15 @@ namespace Barotrauma.Networking
GUIMessageBox interruptPrompt = null; GUIMessageBox interruptPrompt = null;
while (true) if (includesFinalize)
{ {
try ReadStartGameFinalize(inc);
}
else
{
while (true)
{ {
if (timeOut.HasValue) try
{ {
if (DateTime.Now > requestFinalizeTime) if (DateTime.Now > requestFinalizeTime)
{ {
@@ -1583,41 +1588,30 @@ namespace Barotrauma.Networking
return true; return true;
}; };
} }
}
else if (!connected)
{
if (includesFinalize)
{ {
ReadStartGameFinalize(inc); roundInitStatus = RoundInitStatus.Interrupted;
break; break;
} }
//wait for up to 30 seconds for the server to send the STARTGAMEFINALIZE message if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; }
timeOut = DateTime.Now + timeOutDuration;
} }
catch (Exception e)
if (!connected)
{ {
roundInitStatus = RoundInitStatus.Interrupted; DebugConsole.ThrowError("There was an error initializing the round.", e, true);
roundInitStatus = RoundInitStatus.Error;
break; break;
} }
if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } //waiting for a STARTGAMEFINALIZE message
yield return CoroutineStatus.Running;
} }
catch (Exception e)
{
DebugConsole.ThrowError("There was an error initializing the round.", e, true);
roundInitStatus = RoundInitStatus.Error;
break;
}
//waiting for a STARTGAMEFINALIZE message
yield return CoroutineStatus.Running;
} }
interruptPrompt?.Close(); interruptPrompt?.Close();
interruptPrompt = null; interruptPrompt = null;
if (roundInitStatus != RoundInitStatus.Started) if (roundInitStatus != RoundInitStatus.Started)
{ {
if (roundInitStatus != RoundInitStatus.Interrupted) if (roundInitStatus != RoundInitStatus.Interrupted)
@@ -1767,7 +1761,7 @@ namespace Barotrauma.Networking
{ {
string subName = inc.ReadString(); string subName = inc.ReadString();
string subHash = inc.ReadString(); string subHash = inc.ReadString();
byte subClass = inc.ReadByte(); SubmarineClass subClass = (SubmarineClass)inc.ReadByte();
bool isShuttle = inc.ReadBoolean(); bool isShuttle = inc.ReadBoolean();
bool requiredContentPackagesInstalled = inc.ReadBoolean(); bool requiredContentPackagesInstalled = inc.ReadBoolean();
@@ -1776,7 +1770,7 @@ namespace Barotrauma.Networking
{ {
matchingSub = new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) + ".sub", subHash, tryLoad: false) matchingSub = new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) + ".sub", subHash, tryLoad: false)
{ {
SubmarineClass = (SubmarineClass)subClass SubmarineClass = subClass
}; };
if (isShuttle) { matchingSub.AddTag(SubmarineTag.Shuttle); } if (isShuttle) { matchingSub.AddTag(SubmarineTag.Shuttle); }
} }
@@ -2009,10 +2003,10 @@ namespace Barotrauma.Networking
GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled);
GameMain.NetLobbyScreen.SetMissionType(missionType); GameMain.NetLobbyScreen.SetMissionType(missionType);
if (!allowModeVoting) GameMain.NetLobbyScreen.SelectMode(modeIndex); GameMain.NetLobbyScreen.SelectMode(modeIndex);
if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
{ {
if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); if (GameMain.Client.IsServerOwner) { RequestSelectMode(modeIndex); }
} }
if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
@@ -2101,13 +2095,12 @@ namespace Barotrauma.Networking
case ServerNetSegment.EntityPosition: case ServerNetSegment.EntityPosition:
inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
bool isItem = inc.ReadBoolean(); inc.ReadPadBits();
UInt32 incomingUintIdentifier = inc.ReadUInt32();
UInt16 id = inc.ReadUInt16();
uint msgLength = inc.ReadVariableUInt32(); uint msgLength = inc.ReadVariableUInt32();
int msgEndPos = (int)(inc.BitPosition + msgLength * 8); int msgEndPos = (int)(inc.BitPosition + msgLength * 8);
var entity = Entity.FindEntityByID(id) as IServerPositionSync; var header = INetSerializableStruct.Read<EntityPositionHeader>(inc);
var entity = Entity.FindEntityByID(header.EntityId) as IServerPositionSync;
if (msgEndPos > inc.LengthBits) if (msgEndPos > inc.LengthBits)
{ {
DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer.");
@@ -2117,15 +2110,15 @@ namespace Barotrauma.Networking
debugEntityList.Add(entity); debugEntityList.Add(entity);
if (entity != null) if (entity != null)
{ {
if (entity is Item != isItem) if (entity is Item != header.IsItem)
{ {
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(header.IsItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message...");
} }
else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && else if (entity is MapEntity { Prefab.UintIdentifier: var uintIdentifier } me &&
uintIdentifier != incomingUintIdentifier) uintIdentifier != header.PrefabUintIdentifier)
{ {
DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message."
+$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == header.PrefabUintIdentifier)?.Identifier.Value ?? "[not found]"}, "
+$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); +$"client entity is {me.Prefab.Identifier}). Ignoring the message...");
} }
else else
@@ -2133,7 +2126,6 @@ namespace Barotrauma.Networking
entity.ClientReadPosition(inc, sendingTime); entity.ClientReadPosition(inc, sendingTime);
} }
} }
//force to the correct position in case the entity doesn't exist //force to the correct position in case the entity doesn't exist
//or the message wasn't read correctly for whatever reason //or the message wasn't read correctly for whatever reason
inc.BitPosition = msgEndPos; inc.BitPosition = msgEndPos;
@@ -2144,7 +2136,7 @@ namespace Barotrauma.Networking
break; break;
case ServerNetSegment.EntityEvent: case ServerNetSegment.EntityEvent:
case ServerNetSegment.EntityEventInitial: case ServerNetSegment.EntityEventInitial:
if (!EntityEventManager.Read(segment, inc, sendingTime, debugEntityList)) if (!EntityEventManager.Read(segment, inc, sendingTime))
{ {
return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes; return SegmentTableReader<ServerNetSegment>.BreakSegmentReading.Yes;
} }
@@ -2413,7 +2405,9 @@ namespace Barotrauma.Networking
var newSub = new SubmarineInfo(transfer.FilePath); var newSub = new SubmarineInfo(transfer.FilePath);
if (newSub.IsFileCorrupted) { return; } if (newSub.IsFileCorrupted) { return; }
var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation).ToList(); var existingSubs = SubmarineInfo.SavedSubmarines
.Where(s => s.Name == newSub.Name && s.MD5Hash == newSub.MD5Hash)
.ToList();
foreach (SubmarineInfo existingSub in existingSubs) foreach (SubmarineInfo existingSub in existingSubs)
{ {
existingSub.Dispose(); existingSub.Dispose();
@@ -2472,12 +2466,13 @@ namespace Barotrauma.Networking
} }
// Replace a submarine dud with the downloaded version // Replace a submarine dud with the downloaded version
SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.StringRepresentation == newSub.MD5Hash?.StringRepresentation); SubmarineInfo existingServerSub = ServerSubmarines.Find(s =>
s.Name == newSub.Name
&& s.MD5Hash == newSub.MD5Hash);
if (existingServerSub != null) if (existingServerSub != null)
{ {
int existingIndex = ServerSubmarines.IndexOf(existingServerSub); int existingIndex = ServerSubmarines.IndexOf(existingServerSub);
ServerSubmarines.RemoveAt(existingIndex); ServerSubmarines[existingIndex] = newSub;
ServerSubmarines.Insert(existingIndex, newSub);
existingServerSub.Dispose(); existingServerSub.Dispose();
} }
@@ -2798,7 +2793,6 @@ namespace Barotrauma.Networking
/// </summary> /// </summary>
public void RequestSelectMode(int modeIndex) public void RequestSelectMode(int modeIndex)
{ {
if (!HasPermission(ClientPermissions.SelectMode)) return;
if (modeIndex < 0 || modeIndex >= GameMain.NetLobbyScreen.ModeList.Content.CountChildren) if (modeIndex < 0 || modeIndex >= GameMain.NetLobbyScreen.ModeList.Content.CountChildren)
{ {
DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace.CleanupStackTrace()); DebugConsole.ThrowError("Gamemode index out of bounds (" + modeIndex + ")\n" + Environment.StackTrace.CleanupStackTrace());
@@ -2852,13 +2846,14 @@ namespace Barotrauma.Networking
/// <summary> /// <summary>
/// Tell the server to end the round (permission required) /// Tell the server to end the round (permission required)
/// </summary> /// </summary>
public void RequestRoundEnd(bool save) public void RequestRoundEnd(bool save, bool quitCampaign = false)
{ {
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND);
msg.WriteUInt16((UInt16)ClientPermissions.ManageRound); msg.WriteUInt16((UInt16)ClientPermissions.ManageRound);
msg.WriteBoolean(true); //indicates round end msg.WriteBoolean(true); //indicates round end
msg.WriteBoolean(save); msg.WriteBoolean(save);
msg.WriteBoolean(quitCampaign);
ClientPeer.Send(msg, DeliveryMethod.Reliable); ClientPeer.Send(msg, DeliveryMethod.Reliable);
} }
@@ -109,16 +109,15 @@ namespace Barotrauma.Networking
private UInt16? firstNewID; private UInt16? firstNewID;
private readonly List<IServerSerializable> tempEntityList = new List<IServerSerializable>();
/// <summary> /// <summary>
/// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails. /// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails.
/// </summary> /// </summary>
public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime, List<IServerSerializable> entities) public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime)
{ {
UInt16 unreceivedEntityEventCount = 0;
if (type == ServerNetSegment.EntityEventInitial) if (type == ServerNetSegment.EntityEventInitial)
{ {
unreceivedEntityEventCount = msg.ReadUInt16(); UInt16 unreceivedEntityEventCount = msg.ReadUInt16();
firstNewID = msg.ReadUInt16(); firstNewID = msg.ReadUInt16();
if (GameSettings.CurrentConfig.VerboseLogging) if (GameSettings.CurrentConfig.VerboseLogging)
@@ -143,7 +142,7 @@ namespace Barotrauma.Networking
} }
} }
entities.Clear(); tempEntityList.Clear();
msg.ReadPadBits(); msg.ReadPadBits();
UInt16 firstEventID = msg.ReadUInt16(); UInt16 firstEventID = msg.ReadUInt16();
@@ -156,9 +155,9 @@ namespace Barotrauma.Networking
{ {
string errorMsg = $"Error while reading a message from the server. Entity event data exceeds the size of the buffer (current position: {msg.BitPosition}, length: {msg.LengthBits})."; string errorMsg = $"Error while reading a message from the server. Entity event data exceeds the size of the buffer (current position: {msg.BitPosition}, length: {msg.LengthBits}).";
errorMsg += "\nPrevious entities:"; errorMsg += "\nPrevious entities:";
for (int j = entities.Count - 1; j >= 0; j--) for (int j = tempEntityList.Count - 1; j >= 0; j--)
{ {
errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); errorMsg += "\n" + (tempEntityList[j] == null ? "NULL" : tempEntityList[j].ToString());
} }
DebugConsole.ThrowError(errorMsg); DebugConsole.ThrowError(errorMsg);
return false; return false;
@@ -174,7 +173,7 @@ namespace Barotrauma.Networking
DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)",
Microsoft.Xna.Framework.Color.Orange); Microsoft.Xna.Framework.Color.Orange);
} }
entities.Add(null); tempEntityList.Add(null);
if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; } if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; }
continue; continue;
} }
@@ -182,7 +181,7 @@ namespace Barotrauma.Networking
int msgLength = (int)msg.ReadVariableUInt32(); int msgLength = (int)msg.ReadVariableUInt32();
IServerSerializable entity = Entity.FindEntityByID(entityID) as IServerSerializable; IServerSerializable entity = Entity.FindEntityByID(entityID) as IServerSerializable;
entities.Add(entity); tempEntityList.Add(entity);
//skip the event if we've already received it or if the entity isn't found //skip the event if we've already received it or if the entity isn't found
if (thisEventID != (UInt16)(lastReceivedID + 1) || entity == null) if (thisEventID != (UInt16)(lastReceivedID + 1) || entity == null)
@@ -223,7 +222,7 @@ namespace Barotrauma.Networking
if (msg.BitPosition != msgPosition + msgLength * 8) if (msg.BitPosition != msgPosition + msgLength * 8)
{ {
var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; var prevEntity = tempEntityList.Count >= 2 ? tempEntityList[tempEntityList.Count - 2] : null;
ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0;
string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). "
+$"The previous entity was \"{prevEntity}\" (ID {prevId}) " +$"The previous entity was \"{prevEntity}\" (ID {prevId}) "
@@ -30,6 +30,8 @@ namespace Barotrauma.Networking
protected readonly bool isOwner; protected readonly bool isOwner;
protected readonly Option<int> ownerKey; protected readonly Option<int> ownerKey;
public bool IsActive => isActive;
protected bool isActive; protected bool isActive;
public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option<int> ownerKey) public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option<int> ownerKey)
@@ -98,16 +98,15 @@ namespace Barotrauma
foreach (GUIComponent comp in listBox.Content.Children) foreach (GUIComponent comp in listBox.Content.Children)
{ {
if (comp.UserData != userData) { continue; } if (comp.UserData != userData) { continue; }
if (!(comp.FindChild("votes") is GUITextBlock voteText)) if (comp.FindChild("votes") is not GUITextBlock voteText)
{ {
voteText = new GUITextBlock(new RectTransform(new Point(30, comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), voteText = new GUITextBlock(new RectTransform(new Point(GUI.IntScale(30), comp.Rect.Height), comp.RectTransform, Anchor.CenterRight),
"", textAlignment: Alignment.CenterRight) "", textAlignment: Alignment.Center)
{ {
Padding = Vector4.Zero, Padding = Vector4.Zero,
UserData = "votes" UserData = "votes"
}; };
} }
voteText.Text = votes == 0 ? "" : votes.ToString(); voteText.Text = votes == 0 ? "" : votes.ToString();
} }
} }
@@ -303,7 +303,7 @@ namespace Barotrauma
bool ChangeValue(GUIButton btn, object userData) bool ChangeValue(GUIButton btn, object userData)
{ {
if (!(userData is int change)) { return false; } if (userData is not int change) { return false; }
int hiddenOptions = 0; int hiddenOptions = 0;
@@ -365,7 +365,7 @@ namespace Barotrauma
private void CreateCustomizeWindow(CampaignSettings prevSettings, Action<CampaignSettings> onClosed = null) private void CreateCustomizeWindow(CampaignSettings prevSettings, Action<CampaignSettings> onClosed = null)
{ {
CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.3f), minSize: new Point(450, 350)); CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.5f), minSize: new Point(450, 350));
GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter));
@@ -125,7 +125,7 @@ namespace Barotrauma.CharacterEditor
{ {
ResetVariables(); ResetVariables();
var subInfo = new SubmarineInfo("Content/AnimEditor.sub"); var subInfo = new SubmarineInfo("Content/AnimEditor.sub");
Submarine.MainSub = new Submarine(subInfo); Submarine.MainSub = new Submarine(subInfo, showErrorMessages: false);
if (Submarine.MainSub.PhysicsBody != null) if (Submarine.MainSub.PhysicsBody != null)
{ {
Submarine.MainSub.PhysicsBody.Enabled = false; Submarine.MainSub.PhysicsBody.Enabled = false;
@@ -162,11 +162,6 @@ namespace Barotrauma.CharacterEditor
OpenDoors(); OpenDoors();
GameMain.Instance.ResolutionChanged += OnResolutionChanged; GameMain.Instance.ResolutionChanged += OnResolutionChanged;
Instance = this; Instance = this;
if (!GameSettings.CurrentConfig.EditorDisclaimerShown)
{
GameMain.Instance.ShowEditorDisclaimer();
}
} }
private void ResetVariables() private void ResetVariables()
@@ -2688,10 +2683,6 @@ namespace Barotrauma.CharacterEditor
// Character selection // Character selection
var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont);
var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.2f, 0.7f), characterLabel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton")
{
OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; }
};
var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform) var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform)
{ {
@@ -820,6 +820,15 @@ namespace Barotrauma
}; };
valueInput.Text = newValue?.ToString() ?? "<type here>"; valueInput.Text = newValue?.ToString() ?? "<type here>";
} }
else if (type == typeof(Identifier))
{
GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? string.Empty);
valueInput.OnTextChanged += (component, o) =>
{
newValue = new Identifier(o);
return true;
};
}
else if (type == typeof(float) || type == typeof(int)) else if (type == typeof(float) || type == typeof(int))
{ {
GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) };
@@ -51,9 +51,8 @@ namespace Barotrauma
private readonly GUIFrame modsButtonContainer; private readonly GUIFrame modsButtonContainer;
private readonly GUIButton modsButton, modUpdatesButton; private readonly GUIButton modsButton, modUpdatesButton;
private Task<IReadOnlyList<Steamworks.Ugc.Item>> modUpdateTask; private (DateTime WhenToRefresh, int Count) modUpdateStatus = (DateTime.Now, 0);
private float modUpdateTimer = 0.0f; private static readonly TimeSpan ModUpdateInterval = TimeSpan.FromSeconds(60.0f);
private const float ModUpdateInterval = 60.0f;
private readonly GameMain game; private readonly GameMain game;
@@ -736,8 +735,7 @@ namespace Barotrauma
public void ResetModUpdateButton() public void ResetModUpdateButton()
{ {
modUpdateTask = null; modUpdateStatus = (DateTime.Now, 0);
modUpdateTimer = 0;
modUpdatesButton.Visible = false; modUpdatesButton.Visible = false;
} }
@@ -875,7 +873,25 @@ namespace Barotrauma
GameMain.ResetNetLobbyScreen(); GameMain.ResetNetLobbyScreen();
try try
{ {
string exeName = serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f ? f.Path.Value : "DedicatedServer"; string fileName;
if (serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f &&
f.ContentPackage != GameMain.VanillaContent)
{
fileName = Path.Combine(
Path.GetDirectoryName(f.Path.Value),
Path.GetFileNameWithoutExtension(f.Path.Value));
#if WINDOWS
fileName += ".exe";
#endif
}
else
{
#if WINDOWS
fileName = "DedicatedServer.exe";
#else
fileName = "./DedicatedServer";
#endif
}
string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" +
" -public " + isPublicBox.Selected.ToString() + " -public " + isPublicBox.Selected.ToString() +
@@ -899,19 +915,10 @@ namespace Barotrauma
} }
int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1);
arguments += " -ownerkey " + ownerKey; arguments += " -ownerkey " + ownerKey;
string filename = Path.Combine(
Path.GetDirectoryName(exeName),
Path.GetFileNameWithoutExtension(exeName));
#if WINDOWS
filename += ".exe";
#else
filename = "./" + exeName;
#endif
var processInfo = new ProcessStartInfo var processInfo = new ProcessStartInfo
{ {
FileName = filename, FileName = fileName,
Arguments = arguments, Arguments = arguments,
WorkingDirectory = Directory.GetCurrentDirectory(), WorkingDirectory = Directory.GetCurrentDirectory(),
#if !DEBUG #if !DEBUG
@@ -958,15 +965,42 @@ namespace Barotrauma
} }
} }
private void UpdateOutOfDateWorkshopItemCount()
{
if (DateTime.Now < modUpdateStatus.WhenToRefresh) { return; }
if (!SteamManager.IsInitialized) { return; }
var installedPackages = ContentPackageManager.WorkshopPackages;
var ids = SteamManager.Workshop.GetSubscribedItemIds()
.Select(id => id.Value)
.Union(installedPackages
.Select(pkg => pkg.UgcId)
.NotNone()
.OfType<SteamWorkshopId>()
.Select(id => id.Value));
var count = ids
// Deliberately construct Steamworks.Ugc.Item directly
// to not immediately generate a Workshop data request
.Select(id => new Steamworks.Ugc.Item(id))
.Count(item =>
installedPackages.FirstOrDefault(p
=> p.UgcId.TryUnwrap(out SteamWorkshopId id) && id.Value == item.Id)
is { } pkg
// Checking that this item is downloading, waiting to be downloaded
// or is newer than the currently installed copy should be good enough,
// and should still not make a Workshop data request
&& (item.IsDownloading
|| item.IsDownloadPending
|| (item.InstallTime.TryGetValue(out var workshopInstallTime)
&& pkg.InstallTime.TryUnwrap(out var localInstallTime)
&& localInstallTime < workshopInstallTime)));
modUpdateStatus = (DateTime.Now + ModUpdateInterval, count);
}
public override void Update(double deltaTime) public override void Update(double deltaTime)
{ {
modUpdateTimer -= (float)deltaTime;
if (modUpdateTimer <= 0.0f && modUpdateTask is not { IsCompleted: false })
{
modUpdateTask = BulkDownloader.GetItemsThatNeedUpdating();
modUpdateTimer = ModUpdateInterval;
}
#if DEBUG #if DEBUG
hostServerButton.Enabled = true; hostServerButton.Enabled = true;
#else #else
@@ -976,10 +1010,8 @@ namespace Barotrauma
} }
#endif #endif
if (modUpdateTask is { IsCompletedSuccessfully: true }) UpdateOutOfDateWorkshopItemCount();
{ modUpdatesButton.Visible = modUpdateStatus.Count > 0;
modUpdatesButton.Visible = modUpdateTask.Result.Count > 0;
}
if (modUpdatesButton.Visible) if (modUpdatesButton.Visible)
{ {
@@ -189,7 +189,8 @@ namespace Barotrauma
} }
public IReadOnlyList<SubmarineInfo> GetSubList() public IReadOnlyList<SubmarineInfo> GetSubList()
=> SubList.Content.Children.Select(c => c.UserData as SubmarineInfo).ToArray(); => (IReadOnlyList<SubmarineInfo>)GameMain.Client?.ServerSubmarines
?? Array.Empty<SubmarineInfo>();
public readonly GUIListBox PlayerList; public readonly GUIListBox PlayerList;
@@ -929,6 +930,8 @@ namespace Barotrauma
var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont);
var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true);
//leave some padding for the vote count text
modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W);
modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent;
modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor;
modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f;
@@ -981,7 +984,7 @@ namespace Barotrauma
} }
else else
{ {
GameMain.Client.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true);
} }
return true; return true;
} }
@@ -3291,16 +3294,15 @@ namespace Barotrauma
{ {
//campaign running //campaign running
settingsBlocker.Visible = true; settingsBlocker.Visible = true;
CampaignFrame.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound);
ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.HasPermission(ClientPermissions.ManageRound)); ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && CampaignFrame.Visible;
QuitCampaignButton.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign);
CampaignSetupFrame.Visible = false; CampaignSetupFrame.Visible = false;
} }
else else
{ {
CampaignFrame.Visible = false; CampaignFrame.Visible = false;
CampaignSetupFrame.Visible = true; CampaignSetupFrame.Visible = true;
if (!GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)) if (!CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound))
{ {
CampaignSetupFrame.ClearChildren(); CampaignSetupFrame.ClearChildren();
new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center),
@@ -3364,7 +3366,7 @@ namespace Barotrauma
CampaignFrame.Visible = CampaignSetupFrame.Visible = false; CampaignFrame.Visible = CampaignSetupFrame.Visible = false;
} }
RefreshEnabledElements(); RefreshEnabledElements();
if (enabled) if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign)
{ {
ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes);
} }
@@ -543,13 +543,6 @@ namespace Barotrauma
} }
}; };
var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), paddedTopPanel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton")
{
IgnoreLayoutGroups = true,
OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; }
};
disclaimerBtn.RectTransform.MaxSize = new Point(disclaimerBtn.Rect.Height);
TopPanel.RectTransform.MinSize = new Point(0, (int)(paddedTopPanel.RectTransform.Children.Max(c => c.MinSize.Y) / paddedTopPanel.RectTransform.RelativeSize.Y)); TopPanel.RectTransform.MinSize = new Point(0, (int)(paddedTopPanel.RectTransform.Children.Max(c => c.MinSize.Y) / paddedTopPanel.RectTransform.RelativeSize.Y));
paddedTopPanel.Recalculate(); paddedTopPanel.Recalculate();
@@ -1425,7 +1418,7 @@ namespace Barotrauma
else if (MainSub == null) else if (MainSub == null)
{ {
var subInfo = new SubmarineInfo(); var subInfo = new SubmarineInfo();
MainSub = new Submarine(subInfo); MainSub = new Submarine(subInfo, showErrorMessages: false);
} }
MainSub.UpdateTransform(interpolate: false); MainSub.UpdateTransform(interpolate: false);
@@ -1462,11 +1455,6 @@ namespace Barotrauma
ImageManager.OnEditorSelected(); ImageManager.OnEditorSelected();
ReconstructLayers(); ReconstructLayers();
if (!GameSettings.CurrentConfig.EditorDisclaimerShown)
{
GameMain.Instance.ShowEditorDisclaimer();
}
} }
public override void OnFileDropped(string filePath, string extension) public override void OnFileDropped(string filePath, string extension)
@@ -2726,11 +2714,13 @@ namespace Barotrauma
previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y));
var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.06f), rightColumn.RectTransform), isHorizontal: true); var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true);
GUIButton createTabberBtn(string labelTag) GUIButton createTabberBtn(string labelTag)
{ {
var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton"); var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton");
btn.TextBlock.Wrap = true;
btn.TextBlock.SetTextPos();
btn.RectTransform.MaxSize = RectTransform.MaxPoint; btn.RectTransform.MaxSize = RectTransform.MaxPoint;
btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint);
btn.Font = GUIStyle.SmallFont; btn.Font = GUIStyle.SmallFont;
@@ -5635,8 +5625,7 @@ namespace Barotrauma
MouseDragStart = Vector2.Zero; MouseDragStart = Vector2.Zero;
} }
if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) if ((GUI.MouseOn == null || !GUI.MouseOn.IsChildOf(TopPanel))
&& !snapToGridFrame.Rect.Contains(PlayerInput.MousePosition)
&& dummyCharacter?.SelectedItem == null && !WiringMode && dummyCharacter?.SelectedItem == null && !WiringMode
&& (GUI.MouseOn == null || MapEntity.SelectedAny || MapEntity.SelectionPos != Vector2.Zero)) && (GUI.MouseOn == null || MapEntity.SelectedAny || MapEntity.SelectionPos != Vector2.Zero))
{ {
@@ -779,6 +779,8 @@ namespace Barotrauma
workshopMenu = Screen.Selected is MainMenuScreen workshopMenu = Screen.Selected is MainMenuScreen
? (WorkshopMenu)new MutableWorkshopMenu(content) ? (WorkshopMenu)new MutableWorkshopMenu(content)
: (WorkshopMenu)new ImmutableWorkshopMenu(content); : (WorkshopMenu)new ImmutableWorkshopMenu(content);
GameMain.MainMenuScreen.ResetModUpdateButton();
} }
private void CreateBottomButtons() private void CreateBottomButtons()
@@ -648,6 +648,12 @@ namespace Barotrauma.Sounds
if (isConnected == 0) if (isConnected == 0)
{ {
if (!GameMain.Instance.HasLoaded)
{
//wait for loading to finish so we don't start releasing and reloading sounds when they're being loaded,
//or throw an error mid-loading that'd prevent the content package from being enabled
return;
}
DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings.");
SetAudioOutputDevice("<disconnected>"); SetAudioOutputDevice("<disconnected>");
Disconnected = true; Disconnected = true;
@@ -865,17 +865,18 @@ namespace Barotrauma
public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable<Identifier> tags = null) public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable<Identifier> tags = null)
{ {
var suitableSounds = damageSounds.Where(s =>
s.DamageType == damageType &&
(s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag))));
//if the damage is too low for any sound, don't play anything //if the damage is too low for any sound, don't play anything
if (damageSounds.All(d => damage < d.DamageRange.X)) { return; } if (suitableSounds.All(d => damage < d.DamageRange.X)) { return; }
//allow the damage to differ by 10 from the configured damage range, //allow the damage to differ by 10 from the configured damage range,
//so the same amount of damage doesn't always play the same sound //so the same amount of damage doesn't always play the same sound
float randomizedDamage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); float randomizedDamage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f);
suitableSounds = suitableSounds.Where(s =>
var suitableSounds = damageSounds.Where(s => s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y));
s.DamageType == damageType &&
(s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)) &&
(s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag))));
var damageSound = suitableSounds.GetRandomUnsynced(); var damageSound = suitableSounds.GetRandomUnsynced();
damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null));
@@ -45,18 +45,26 @@ namespace Barotrauma.Steam
protected static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]", protected static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
protected static GUICustomComponent CreateBBCodeElement(string bbCode, GUIListBox container) protected static void CreateBBCodeElement(Steamworks.Ugc.Item workshopItem, GUIListBox container)
{ {
Point cachedContainerSize = Point.Zero; Point cachedContainerSize = Point.Zero;
List<BBWord> bbWords = new List<BBWord>(); List<BBWord> bbWords = new List<BBWord>();
Stack<BBWord.TagType> tagStack = new Stack<BBWord.TagType>(); Stack<BBWord.TagType> tagStack = new Stack<BBWord.TagType>();
void recalculate() string bbCode = "";
void forceReset()
{ {
if (cachedContainerSize == container.Content.RectTransform.NonScaledSize) { return; } bbWords.Clear();
cachedContainerSize = Point.Zero;
}
void recalculate(GUICustomComponent component)
{
if (cachedContainerSize == component.RectTransform.NonScaledSize) { return; }
bbWords.Clear(); bbWords.Clear();
cachedContainerSize = container.Content.RectTransform.NonScaledSize; cachedContainerSize = component.RectTransform.NonScaledSize;
var matches = new Stack<Match>(bbTagRegex.Matches(bbCode).Reverse()); var matches = new Stack<Match>(bbTagRegex.Matches(bbCode).Reverse());
Match? nextTag = null; Match? nextTag = null;
@@ -133,11 +141,14 @@ namespace Barotrauma.Steam
{ {
bbWords.Add(new BBWord(finalWord, currTagType)); bbWords.Add(new BBWord(finalWord, currTagType));
} }
container.RecalculateChildren();
container.UpdateScrollBarSize();
} }
void draw(SpriteBatch spriteBatch, GUICustomComponent component) void draw(SpriteBatch spriteBatch, GUICustomComponent component)
{ {
recalculate(); recalculate(component);
Vector2 currPos = Vector2.Zero; Vector2 currPos = Vector2.Zero;
Vector2 rectPos = component.Rect.Location.ToVector2(); Vector2 rectPos = component.Rect.Location.ToVector2();
for (int i = 0; i < bbWords.Count; i++) for (int i = 0; i < bbWords.Count; i++)
@@ -180,7 +191,19 @@ namespace Barotrauma.Steam
= component.RectTransform.NonScaledSize.ToVector2() / component.Parent.Rect.Size.ToVector2(); = component.RectTransform.NonScaledSize.ToVector2() / component.Parent.Rect.Size.ToVector2();
} }
return new GUICustomComponent(new RectTransform(Vector2.One, container.Content.RectTransform), TaskPool.Add(
$"GetWorkshopItemLongDescriptionFor{workshopItem.Id.Value}",
SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription: true),
t =>
{
if (!t.TryGetResult(out Steamworks.Ugc.Item? workshopItemWithDescription)) { return; }
bbCode = workshopItemWithDescription?.Description ?? "";
forceReset();
});
new GUICustomComponent(
new RectTransform(Vector2.One, container.Content.RectTransform),
onDraw: draw); onDraw: draw);
} }
} }
@@ -34,7 +34,7 @@ namespace Barotrauma.Steam
if (numSubscribedMods == memSubscribedModCount) { return; } if (numSubscribedMods == memSubscribedModCount) { return; }
memSubscribedModCount = numSubscribedMods; memSubscribedModCount = numSubscribedMods;
var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); var subscribedIds = SteamManager.Workshop.GetSubscribedItemIds();
var installedIds = ContentPackageManager.WorkshopPackages var installedIds = ContentPackageManager.WorkshopPackages
.Select(p => p.UgcId) .Select(p => p.UgcId)
.NotNone() .NotNone()
@@ -773,7 +773,7 @@ namespace Barotrauma.Steam
#endregion #endregion
var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform));
CreateBBCodeElement(workshopItem.Description, descriptionListBox); CreateBBCodeElement(workshopItem, descriptionListBox);
var showInSteamContainer var showInSteamContainer
= new GUIFrame(new RectTransform((1.0f, 0.05f), verticalLayout.RectTransform), style: null); = new GUIFrame(new RectTransform((1.0f, 0.05f), verticalLayout.RectTransform), style: null);
@@ -218,6 +218,20 @@ namespace Barotrauma.Steam
var descriptionTextBox var descriptionTextBox
= ScrollableTextBox(rightTop, 6.0f, workshopItem.Description ?? string.Empty); = ScrollableTextBox(rightTop, 6.0f, workshopItem.Description ?? string.Empty);
if (workshopItem.Id != 0)
{
TaskPool.Add(
$"GetFullDescription{workshopItem.Id}",
SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription: true),
t =>
{
if (!t.TryGetResult(out Steamworks.Ugc.Item? itemWithDescription)) { return; }
descriptionTextBox.Text = itemWithDescription?.Description ?? descriptionTextBox.Text;
descriptionTextBox.Deselect();
});
}
var (leftBottom, _, rightBottom) var (leftBottom, _, rightBottom)
= CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f); = CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f);
leftBottom.Stretch = true; leftBottom.Stretch = true;
@@ -2,7 +2,7 @@
namespace Barotrauma namespace Barotrauma
{ {
partial class UpgradePrefab sealed partial class UpgradePrefab
{ {
public readonly ImmutableArray<DecorativeSprite> DecorativeSprites = new ImmutableArray<DecorativeSprite>(); public readonly ImmutableArray<DecorativeSprite> DecorativeSprites = new ImmutableArray<DecorativeSprite>();
public Sprite Sprite { get; private set; } public Sprite Sprite { get; private set; }
@@ -23,36 +23,55 @@ namespace Barotrauma
public static void ConvertMasterLocalizationKit(string outputTextsDirectory, string outputConversationsDirectory, bool convertConversations) public static void ConvertMasterLocalizationKit(string outputTextsDirectory, string outputConversationsDirectory, bool convertConversations)
{ {
string textFilePath = Path.Combine(infoTextPath, "Texts.csv"); List<string> languages = new List<string>();
string conversationFilePath = Path.Combine(infoTextPath, "NPCConversations.csv"); for (int i = 0; i < 2; i++)
{
string textFilePath;
string outputFileName;
switch (i)
{
case 0:
textFilePath = Path.Combine(infoTextPath, "Texts.csv");
outputFileName = "Vanilla.xml";
break;
case 1:
textFilePath = Path.Combine(infoTextPath, "EditorTexts.csv");
outputFileName = "VanillaEditorTexts.xml";
break;
default:
throw new NotImplementedException();
}
Dictionary<string, List<string>> xmlContent; Dictionary<string, List<string>> xmlContent;
try try
{ {
xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8)); xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8));
} }
catch (Exception e) catch (Exception e)
{ {
DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath, e); DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath, e);
return; return;
} }
if (xmlContent == null) if (xmlContent == null)
{ {
DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath); DebugConsole.ThrowError("InfoText Localization .csv to .xml conversion failed for: " + textFilePath);
return; return;
} }
foreach (string language in xmlContent.Keys) foreach (string language in xmlContent.Keys)
{ {
string languageNoWhitespace = language.Replace(" ", ""); languages.Add(language);
string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"{languageNoWhitespace}/{languageNoWhitespace}Vanilla.xml"); string languageNoWhitespace = language.Replace(" ", "");
File.WriteAllLines(xmlFileFullPath, xmlContent[language], Encoding.UTF8); string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"{languageNoWhitespace}/{languageNoWhitespace}{outputFileName}");
DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath); File.WriteAllLines(xmlFileFullPath, xmlContent[language], Encoding.UTF8);
DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath);
}
} }
if (convertConversations) if (convertConversations)
{ {
string conversationFilePath = Path.Combine(infoTextPath, "NPCConversations.csv");
var conversationLinesAll = File.ReadAllLines(conversationFilePath, Encoding.UTF8); var conversationLinesAll = File.ReadAllLines(conversationFilePath, Encoding.UTF8);
foreach (string language in xmlContent.Keys) foreach (string language in languages)
{ {
List<string> convXmlContent = ConvertConversationsToXML(conversationLinesAll, language); List<string> convXmlContent = ConvertConversationsToXML(conversationLinesAll, language);
if (convXmlContent == null) if (convXmlContent == null)
@@ -61,7 +80,7 @@ namespace Barotrauma
continue; continue;
} }
string languageNoWhitespace = language.Replace(" ", ""); string languageNoWhitespace = language.Replace(" ", "");
string xmlFileFullPath = Path.Combine(outputTextsDirectory, $"NpcConversations_{languageNoWhitespace}.xml"); string xmlFileFullPath = Path.Combine(outputConversationsDirectory, languageNoWhitespace, $"NpcConversations_{languageNoWhitespace}.xml");
File.WriteAllLines(xmlFileFullPath, convXmlContent, Encoding.UTF8); File.WriteAllLines(xmlFileFullPath, convXmlContent, Encoding.UTF8);
DebugConsole.NewMessage("Conversation localization .xml file successfully created at: " + xmlFileFullPath); DebugConsole.NewMessage("Conversation localization .xml file successfully created at: " + xmlFileFullPath);
} }
@@ -339,7 +358,8 @@ namespace Barotrauma
string[] headerSplit = csvContent[0].Split(separator); string[] headerSplit = csvContent[0].Split(separator);
for (int i = 0; i < headerSplit.Length; i++) for (int i = 0; i < headerSplit.Length; i++)
{ {
if (headerSplit[i] == language) if (headerSplit[i] == language ||
(language == "English" && headerSplit[i]== "Line (Original)"))
{ {
languageColumn = i; languageColumn = i;
break; break;
@@ -348,6 +368,7 @@ namespace Barotrauma
xmlContent.Add($"<Conversations identifier=\"vanillaconversations\" Language=\"{language}\" nowhitespace=\"{nowhitespace}\">"); xmlContent.Add($"<Conversations identifier=\"vanillaconversations\" Language=\"{language}\" nowhitespace=\"{nowhitespace}\">");
conversationClosingIndent.Clear();
int conversationStart = 1; int conversationStart = 1;
xmlContent.Add(string.Empty); xmlContent.Add(string.Empty);
@@ -399,9 +420,9 @@ namespace Barotrauma
{ {
string[] nextConversationElement = csvContent[i + 1].Split(separator); string[] nextConversationElement = csvContent[i + 1].Split(separator);
if (nextConversationElement[1] != string.Empty) if (nextConversationElement[3] != string.Empty)
{ {
nextDepth = int.Parse(nextConversationElement[2]); nextDepth = int.Parse(nextConversationElement[3]);
nextIsSubConvo = nextDepth > depthIndex; nextIsSubConvo = nextDepth > depthIndex;
} }
@@ -421,7 +442,12 @@ namespace Barotrauma
} }
else else
{ {
//end of file, close remaining xml tags
xmlContent.Add(element.TrimEnd() + "/>"); xmlContent.Add(element.TrimEnd() + "/>");
for (int j = depthIndex - 1; j >= 0; j--)
{
HandleClosingElements(xmlContent, j);
}
} }
} }
@@ -433,12 +459,12 @@ namespace Barotrauma
private static void HandleClosingElements(List<string> xmlContent, int targetDepth) private static void HandleClosingElements(List<string> xmlContent, int targetDepth)
{ {
if (conversationClosingIndent.Count == 0) return; if (conversationClosingIndent.Count == 0) { return; }
for (int k = conversationClosingIndent.Count - 1; k >= 0; k--) for (int k = conversationClosingIndent.Count - 1; k >= 0; k--)
{ {
int currentIndent = conversationClosingIndent[k]; int currentIndent = conversationClosingIndent[k];
if (currentIndent < targetDepth) break; if (currentIndent < targetDepth) { break; }
xmlContent.Add($"{GetIndenting(currentIndent)}</Conversation>"); xmlContent.Add($"{GetIndenting(currentIndent)}</Conversation>");
conversationClosingIndent.RemoveAt(k); conversationClosingIndent.RemoveAt(k);
} }
@@ -7,28 +7,30 @@ using System.Text;
namespace Barotrauma namespace Barotrauma
{ {
class SpriteRecorder : ISpriteBatch, IDisposable sealed class SpriteRecorder : ISpriteBatch, IDisposable
{ {
private struct Command private readonly record struct Command(
Texture2D Texture,
VertexPositionColorTexture VertexBL,
VertexPositionColorTexture VertexBR,
VertexPositionColorTexture VertexTL,
VertexPositionColorTexture VertexTR,
float Depth,
Vector2 Min,
Vector2 Max,
int Index)
{ {
public readonly Texture2D Texture; public static Vector2 GetMinPosition(params VertexPositionColorTexture[] vertices)
public readonly VertexPositionColorTexture VertexBL; => new Vector2(
public readonly VertexPositionColorTexture VertexBR; MathUtils.Min(vertices.Select(v => v.Position.X).ToArray()),
public readonly VertexPositionColorTexture VertexTL; MathUtils.Min(vertices.Select(v => v.Position.Y).ToArray()));
public readonly VertexPositionColorTexture VertexTR;
public readonly float Depth; public static Vector2 GetMaxPosition(params VertexPositionColorTexture[] vertices)
public readonly Vector2 Min; => new Vector2(
public readonly Vector2 Max; MathUtils.Max(vertices.Select(v => v.Position.X).ToArray()),
public readonly int Index; MathUtils.Max(vertices.Select(v => v.Position.Y).ToArray()));
public bool Overlaps(Command other) public static Command FromTransform(
{
return
Min.X <= other.Max.X && Max.X >= other.Min.X &&
Min.Y <= other.Max.Y && Max.Y >= other.Min.Y;
}
public Command(
Texture2D texture, Texture2D texture,
Vector2 pos, Vector2 pos,
Rectangle srcRect, Rectangle srcRect,
@@ -46,15 +48,11 @@ namespace Barotrauma
int srcRectBottom = srcRect.Bottom; int srcRectBottom = srcRect.Bottom;
if (effects.HasFlag(SpriteEffects.FlipHorizontally)) if (effects.HasFlag(SpriteEffects.FlipHorizontally))
{ {
var temp = srcRectRight; (srcRectRight, srcRectLeft) = (srcRectLeft, srcRectRight);
srcRectRight = srcRectLeft;
srcRectLeft = temp;
} }
if (effects.HasFlag(SpriteEffects.FlipVertically)) if (effects.HasFlag(SpriteEffects.FlipVertically))
{ {
var temp = srcRectBottom; (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom);
srcRectBottom = srcRectTop;
srcRectTop = temp;
} }
rotation = MathHelper.ToRadians(rotation); rotation = MathHelper.ToRadians(rotation);
@@ -68,59 +66,63 @@ namespace Barotrauma
pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin; pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin;
pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin; pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin;
Texture = texture; var vertexTl = new VertexPositionColorTexture
{
Color = color,
Position = new Vector3(pos.X, pos.Y, 0f),
TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height)
};
Depth = depth; var vertexTr = new VertexPositionColorTexture
{
Color = color,
Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f),
TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height)
};
VertexTL.Color = color; var vertexBl = new VertexPositionColorTexture
VertexTR.Color = color; {
VertexBL.Color = color; Color = color,
VertexBR.Color = color; Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f),
TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height)
};
VertexTL.Position = new Vector3(pos.X, pos.Y, 0f); var vertexBr = new VertexPositionColorTexture
VertexTR.Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f); {
VertexBL.Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f); Color = color,
VertexBR.Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f); Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f),
TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height)
};
Min = new Vector2( var min = GetMinPosition(
MathUtils.Min vertexTl,
( vertexTr,
VertexTL.Position.X, vertexBl,
VertexTR.Position.X, vertexBr);
VertexBL.Position.X,
VertexBR.Position.X
),
MathUtils.Min
(
VertexTL.Position.Y,
VertexTR.Position.Y,
VertexBL.Position.Y,
VertexBR.Position.Y
));
Max = new Vector2( var max = GetMaxPosition(
MathUtils.Max vertexTl,
( vertexTr,
VertexTL.Position.X, vertexBl,
VertexTR.Position.X, vertexBr);
VertexBL.Position.X,
VertexBR.Position.X
),
MathUtils.Max
(
VertexTL.Position.Y,
VertexTR.Position.Y,
VertexBL.Position.Y,
VertexBR.Position.Y
));
VertexTL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height); return new Command(
VertexTR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height); texture,
VertexBL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); vertexBl,
VertexBR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); vertexBr,
vertexTl,
Index = index; vertexTr,
depth,
min,
max,
index);
}
public bool Overlaps(Command other)
{
return
Min.X <= other.Max.X && Max.X >= other.Min.X &&
Min.Y <= other.Max.Y && Max.Y >= other.Min.Y;
} }
} }
@@ -151,8 +153,8 @@ namespace Barotrauma
public static BasicEffect BasicEffect = null; public static BasicEffect BasicEffect = null;
private List<RecordedBuffer> recordedBuffers = new List<RecordedBuffer>(); private readonly List<RecordedBuffer> recordedBuffers = new List<RecordedBuffer>();
private List<Command> commandList = new List<Command>(); private readonly List<Command> commandList = new List<Command>();
private SpriteSortMode currentSortMode; private SpriteSortMode currentSortMode;
private IndexBuffer indexBuffer = null; private IndexBuffer indexBuffer = null;
@@ -170,16 +172,45 @@ namespace Barotrauma
currentSortMode = sortMode; currentSortMode = sortMode;
} }
public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) private void AppendCommand(Command command)
{ {
if (isDisposed) { return; } if (isDisposed) { return; }
Command command = new Command(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList?.Count ?? 0);
if (commandList.Count == 0) { Min = command.Min; Max = command.Max; } if (commandList.Count == 0) { Min = command.Min; Max = command.Max; }
Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y)); Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y));
Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y)); Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y));
commandList?.Add(command); commandList.Add(command);
}
public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth)
{
if (isDisposed) { return; }
var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList.Count);
AppendCommand(command);
}
public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth, int? count = null)
{
if (isDisposed) { return; }
int iters = count ?? (vertices.Length / 4);
for (int i=0;i<iters;i++)
{
var subset = vertices[((i * 4) + 0)..((i * 4) + 4)];
var command = new Command(
texture,
subset[2],
subset[3],
subset[0],
subset[1],
layerDepth,
Command.GetMinPosition(subset),
Command.GetMaxPosition(subset),
commandList.Count);
AppendCommand(command);
}
} }
public void End() public void End()
@@ -309,16 +340,13 @@ namespace Barotrauma
public void Dispose() public void Dispose()
{ {
isDisposed = true; isDisposed = true;
if (recordedBuffers != null) foreach (var buffer in recordedBuffers)
{ {
foreach (var buffer in recordedBuffers) buffer.VertexBuffer.Dispose();
{
buffer.VertexBuffer.Dispose();
}
recordedBuffers.Clear(); recordedBuffers = null;
} }
commandList?.Clear(); commandList = null; recordedBuffers.Clear();
indexBuffer?.Dispose(); indexBuffer = null; commandList.Clear();
indexBuffer.Dispose(); indexBuffer = null;
ReadyToRender = false; ReadyToRender = false;
} }
} }
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.20.15.0</Version> <Version>0.21.1.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.20.15.0</Version> <Version>0.21.1.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.20.15.0</Version> <Version>0.21.1.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.20.15.0</Version> <Version>0.21.1.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.20.15.0</Version> <Version>0.21.1.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -8,8 +8,9 @@ namespace Barotrauma
{ {
partial class Character partial class Character
{ {
public Address OwnerClientAddress; private Address ownerClientAddress;
public string OwnerClientName; private Option<AccountId> ownerClientAccountId;
public bool ClientDisconnected; public bool ClientDisconnected;
public float KillDisconnectedTimer; public float KillDisconnectedTimer;
@@ -19,6 +20,35 @@ namespace Barotrauma
public bool HealthUpdatePending; public bool HealthUpdatePending;
public void SetOwnerClient(Client client)
{
if (client == null)
{
ownerClientAddress = null;
ownerClientAccountId = Option<AccountId>.None();
IsRemotePlayer = false;
}
else
{
ownerClientAddress = client.Connection.Endpoint.Address;
ownerClientAccountId = client.AccountId;
IsRemotePlayer = true;
}
}
public bool IsClientOwner(Client client)
{
if (ownerClientAccountId.TryUnwrap(out var accountId)
&& client.AccountId.TryUnwrap(out var clientId))
{
return accountId == clientId;
}
else
{
return ownerClientAddress == client.Connection.Endpoint.Address;
}
}
public float GetPositionUpdateInterval(Client recipient) public float GetPositionUpdateInterval(Client recipient)
{ {
if (!Enabled) { return 1000.0f; } if (!Enabled) { return 1000.0f; }
@@ -302,12 +332,8 @@ namespace Barotrauma
} }
} }
public void ServerWritePosition(IWriteMessage msg, Client c) public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c)
{ {
msg.WriteUInt16(ID);
IWriteMessage tempBuffer = new WriteOnlyMessage();
if (this == c.Character) if (this == c.Character)
{ {
tempBuffer.WriteBoolean(true); tempBuffer.WriteBoolean(true);
@@ -405,11 +431,6 @@ namespace Barotrauma
AIController?.ServerWrite(tempBuffer); AIController?.ServerWrite(tempBuffer);
HealthUpdatePending = false; HealthUpdatePending = false;
} }
tempBuffer.WritePadBits();
msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes);
msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
} }
public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -1374,7 +1374,7 @@ namespace Barotrauma
MultiPlayerCampaign.StartCampaignSetup(); MultiPlayerCampaign.StartCampaignSetup();
return; return;
} }
if (!GameMain.Server.StartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } if (!GameMain.Server.TryStartGame()) { NewMessage("Failed to start a new round", Color.Yellow); }
} }
})); }));
@@ -66,6 +66,7 @@ namespace Barotrauma
public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage; public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage;
public readonly string[] CommandLineArgs; public readonly string[] CommandLineArgs;
public GameMain(string[] args) public GameMain(string[] args)
@@ -27,12 +27,9 @@ namespace Barotrauma
AnyOneAllowedToManageCampaign(permissions); AnyOneAllowedToManageCampaign(permissions);
} }
public bool AllowedToManageWallets(Client client) public static bool AllowedToManageWallets(Client client)
{ {
return return AllowedToManageCampaign(client, ClientPermissions.ManageMoney);
client.HasPermission(ClientPermissions.ManageCampaign) ||
client.HasPermission(ClientPermissions.ManageMoney) ||
IsOwner(client);
} }
public override void ShowStartMessage() public override void ShowStartMessage()
@@ -146,7 +146,7 @@ namespace Barotrauma
{ {
NextLevel = map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData; NextLevel = map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData;
MirrorLevel = false; MirrorLevel = false;
GameMain.Server.StartGame(); GameMain.Server.TryStartGame();
} }
public static void StartCampaignSetup() public static void StartCampaignSetup()
@@ -395,7 +395,7 @@ namespace Barotrauma
yield return new WaitForSeconds(EndTransitionDuration * 0.5f); yield return new WaitForSeconds(EndTransitionDuration * 0.5f);
} }
GameMain.Server.StartGame(); GameMain.Server.TryStartGame();
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
} }
@@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components
msg.WriteBoolean(launch); msg.WriteBoolean(launch);
if (launch) if (launch)
{ {
msg.WriteUInt16(User.ID); msg.WriteUInt16(User?.ID ?? Entity.NullEntityID);
msg.WriteSingle(launchPos.X); msg.WriteSingle(launchPos.X);
msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchPos.Y);
msg.WriteSingle(launchRot); msg.WriteSingle(launchRot);
@@ -348,15 +348,9 @@ namespace Barotrauma
} }
} }
public void ServerWritePosition(IWriteMessage msg, Client c) public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c)
{ {
msg.WriteUInt16(ID);
IWriteMessage tempBuffer = new WriteOnlyMessage();
body.ServerWrite(tempBuffer); body.ServerWrite(tempBuffer);
msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes);
msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
msg.WritePadBits();
} }
public void CreateServerEvent<T>(T ic) where T : ItemComponent, IServerSerializable public void CreateServerEvent<T>(T ic) where T : ItemComponent, IServerSerializable
@@ -378,7 +372,7 @@ namespace Barotrauma
if (!components.Contains(ic)) { return; } if (!components.Contains(ic)) { return; }
var eventData = new ComponentStateEventData(ic, extraData); var eventData = new ComponentStateEventData(ic, extraData);
if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); } if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false."); }
GameMain.Server.CreateEntityEvent(this, eventData); GameMain.Server.CreateEntityEvent(this, eventData);
} }
@@ -5,14 +5,9 @@ namespace Barotrauma
{ {
partial class Submarine partial class Submarine
{ {
public void ServerWritePosition(IWriteMessage msg, Client c) public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c)
{ {
msg.WriteUInt16(ID);
IWriteMessage tempBuffer = new WriteOnlyMessage();
subBody.Body.ServerWrite(tempBuffer); subBody.Body.ServerWrite(tempBuffer);
msg.WriteByte((byte)tempBuffer.LengthBytes);
msg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
msg.WritePadBits();
} }
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -58,6 +58,7 @@ namespace Barotrauma.Networking
private DateTime roundStartTime; private DateTime roundStartTime;
private bool wasReadyToStartAutomatically;
private bool autoRestartTimerRunning; private bool autoRestartTimerRunning;
private float endRoundTimer; private float endRoundTimer;
@@ -366,8 +367,7 @@ namespace Barotrauma.Networking
character.KillDisconnectedTimer += deltaTime; character.KillDisconnectedTimer += deltaTime;
character.SetStun(1.0f); character.SetStun(1.0f);
Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.AddressMatches(character.OwnerClientAddress)); Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c));
if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime)
{ {
character.Kill(CauseOfDeathType.Disconnected, null); character.Kill(CauseOfDeathType.Disconnected, null);
@@ -504,8 +504,7 @@ namespace Barotrauma.Networking
initiatedStartGame = false; initiatedStartGame = false;
} }
} }
else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame && else if (Screen.Selected == GameMain.NetLobbyScreen && !GameStarted && !initiatedStartGame)
(GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign))
{ {
if (ServerSettings.AutoRestart) if (ServerSettings.AutoRestart)
{ {
@@ -526,18 +525,25 @@ namespace Barotrauma.Networking
} }
} }
bool readyToStartAutomatically = false;
if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f) if (ServerSettings.AutoRestart && autoRestartTimerRunning && ServerSettings.AutoRestartTimer < 0.0f)
{ {
StartGame(); readyToStartAutomatically = true;
} }
else if (ServerSettings.StartWhenClientsReady) else if (ServerSettings.StartWhenClientsReady)
{ {
int clientsReady = connectedClients.Count(c => c.GetVote<bool>(VoteType.StartRound)); int clientsReady = connectedClients.Count(c => c.GetVote<bool>(VoteType.StartRound));
if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio) if (clientsReady / (float)connectedClients.Count >= ServerSettings.StartWhenClientsReadyRatio)
{ {
StartGame(); readyToStartAutomatically = true;
} }
} }
if (readyToStartAutomatically)
{
if (!wasReadyToStartAutomatically) { GameMain.NetLobbyScreen.LastUpdateID++; }
TryStartGame();
}
wasReadyToStartAutomatically = readyToStartAutomatically;
} }
for (int i = disconnectedClients.Count - 1; i >= 0; i--) for (int i = disconnectedClients.Count - 1; i >= 0; i--)
@@ -763,7 +769,7 @@ namespace Barotrauma.Networking
else else
{ {
string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{ {
ServerSettings.CampaignSettings = settings; ServerSettings.CampaignSettings = settings;
ServerSettings.SaveSettings(); ServerSettings.SaveSettings();
@@ -779,7 +785,10 @@ namespace Barotrauma.Networking
SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox);
return; return;
} }
if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.LoadCampaign(saveName); } if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{
MultiPlayerCampaign.LoadCampaign(saveName);
}
} }
break; break;
case ClientPacketHeader.VOICE: case ClientPacketHeader.VOICE:
@@ -1389,13 +1398,13 @@ namespace Barotrauma.Networking
if (end) if (end)
{ {
if (mpCampaign == null || if (mpCampaign == null ||
CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound))
CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign))
{ {
bool save = inc.ReadBoolean(); bool save = inc.ReadBoolean();
bool quitCampaign = inc.ReadBoolean();
if (GameStarted) if (GameStarted)
{ {
Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage);
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
{ {
mpCampaign.SavePlayers(); mpCampaign.SavePlayers();
@@ -1409,6 +1418,14 @@ namespace Barotrauma.Networking
} }
EndGame(wasSaved: save); EndGame(wasSaved: save);
} }
else if (mpCampaign != null)
{
Log($"Client \"{ClientLogName(sender)}\" quit the currently active campaign.", ServerLog.MessageType.ServerMessage);
GameMain.GameSession = null;
GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.Sandbox.Identifier;
GameMain.NetLobbyScreen.LastUpdateID++;
}
} }
} }
else else
@@ -1425,12 +1442,11 @@ namespace Barotrauma.Networking
{ {
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath);
} }
} }
else if (!GameStarted && !initiatedStartGame) else if (!GameStarted && !initiatedStartGame)
{ {
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
StartGame(); TryStartGame();
} }
else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)))
{ {
@@ -1492,20 +1508,10 @@ namespace Barotrauma.Networking
case ClientPermissions.SelectMode: case ClientPermissions.SelectMode:
UInt16 modeIndex = inc.ReadUInt16(); UInt16 modeIndex = inc.ReadUInt16();
GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex; GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex;
Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name.Value, ServerLog.MessageType.ServerMessage); Log("Gamemode changed to " + (GameMain.NetLobbyScreen.SelectedMode?.Name.Value ?? "none"), ServerLog.MessageType.ServerMessage);
if (GameMain.NetLobbyScreen.GameModes[modeIndex] == GameModePreset.MultiPlayerCampaign)
if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier == "multiplayercampaign")
{ {
const int MaxSaves = 255; TrySendCampaignSetupInfo(sender);
var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false);
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO);
msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves));
for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
{
msg.WriteNetSerializableStruct(saveInfos[i]);
}
serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable);
} }
break; break;
case ClientPermissions.ManageCampaign: case ClientPermissions.ManageCampaign:
@@ -1612,7 +1618,7 @@ namespace Barotrauma.Networking
{ {
if (GameSettings.CurrentConfig.VerboseLogging) if (GameSettings.CurrentConfig.VerboseLogging)
{ {
DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); DebugConsole.NewMessage($"Sending initial lobby update to {c.Name}", Color.Gray);
} }
outmsg.WriteByte(c.SessionId); outmsg.WriteByte(c.SessionId);
@@ -1745,9 +1751,9 @@ namespace Barotrauma.Networking
continue; continue;
} }
IWriteMessage tempBuffer = new ReadWriteMessage(); var tempBuffer = new ReadWriteMessage();
tempBuffer.WriteBoolean(entity is Item); tempBuffer.WritePadBits(); var entityPositionHeader = EntityPositionHeader.FromEntity(entity);
tempBuffer.WriteUInt32(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); tempBuffer.WriteNetSerializableStruct(entityPositionHeader);
entityPositionSync.ServerWritePosition(tempBuffer, c); entityPositionSync.ServerWritePosition(tempBuffer, c);
//no more room in this packet //no more room in this packet
@@ -1758,6 +1764,7 @@ namespace Barotrauma.Networking
segmentTable.StartNewSegment(ServerNetSegment.EntityPosition); segmentTable.StartNewSegment(ServerNetSegment.EntityPosition);
outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly
outmsg.WriteVariableUInt32((uint)tempBuffer.LengthBytes);
outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WriteBytes(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
outmsg.WritePadBits(); outmsg.WritePadBits();
@@ -1934,6 +1941,13 @@ namespace Barotrauma.Networking
{ {
outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f); outmsg.WriteSingle(autoRestartTimerRunning ? ServerSettings.AutoRestartTimer : 0.0f);
} }
if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign &&
connectedClients.None(c => c.Connection == OwnerConnection || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign)))
{
//if no-one has permissions to manage the campaign, show the setup UI to everyone
TrySendCampaignSetupInfo(c);
}
} }
else else
{ {
@@ -1943,9 +1957,8 @@ namespace Barotrauma.Networking
settingsBytes = outmsg.LengthBytes - settingsBytes; settingsBytes = outmsg.LengthBytes - settingsBytes;
int campaignBytes = outmsg.LengthBytes; int campaignBytes = outmsg.LengthBytes;
var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
if (outmsg.LengthBytes < MsgConstants.MTU - 500 && if (outmsg.LengthBytes < MsgConstants.MTU - 500 &&
campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode)
{ {
outmsg.WriteBoolean(true); outmsg.WriteBoolean(true);
outmsg.WritePadBits(); outmsg.WritePadBits();
@@ -2023,7 +2036,7 @@ namespace Barotrauma.Networking
} }
} }
private void WriteChatMessages(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage outmsg, Client c) private static void WriteChatMessages(in SegmentTableWriter<ServerNetSegment> segmentTable, IWriteMessage outmsg, Client c)
{ {
c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID));
for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
@@ -2037,10 +2050,26 @@ namespace Barotrauma.Networking
} }
} }
public bool StartGame() public bool TryStartGame()
{ {
if (initiatedStartGame || GameStarted) { return false; } if (initiatedStartGame || GameStarted) { return false; }
GameModePreset selectedMode =
Voting.HighestVoted<GameModePreset>(VoteType.Mode, connectedClients) ?? GameMain.NetLobbyScreen.SelectedMode;
if (selectedMode == null)
{
return false;
}
if (selectedMode == GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is not MultiPlayerCampaign)
{
//DebugConsole.ThrowError($"{nameof(TryStartGame)} failed. Cannot start a multiplayer campaign via {nameof(TryStartGame)} - use {nameof(MultiPlayerCampaign.StartNewCampaign)} or {nameof(MultiPlayerCampaign.LoadCampaign)} instead.");
if (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign)
{
GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.MultiPlayerCampaign.Identifier;
}
return false;
}
Log("Starting a new round...", ServerLog.MessageType.ServerMessage); Log("Starting a new round...", ServerLog.MessageType.ServerMessage);
SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle;
@@ -2060,23 +2089,13 @@ namespace Barotrauma.Networking
return false; return false;
} }
GameModePreset selectedMode = Voting.HighestVoted<GameModePreset>(VoteType.Mode, connectedClients);
if (selectedMode == null) { selectedMode = GameMain.NetLobbyScreen.SelectedMode; }
if (selectedMode == null)
{
return false;
}
if (selectedMode == GameModePreset.MultiPlayerCampaign && !(GameMain.GameSession?.GameMode is CampaignMode))
{
DebugConsole.ThrowError("StartGame failed. Cannot start a multiplayer campaign via StartGame - use MultiPlayerCampaign.StartNewCampaign or MultiPlayerCampaign.LoadCampaign instead.");
return false;
}
initiatedStartGame = true; initiatedStartGame = true;
startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame");
return true; return true;
} }
private IEnumerable<CoroutineStatus> InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) private IEnumerable<CoroutineStatus> InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode)
{ {
initiatedStartGame = true; initiatedStartGame = true;
@@ -2168,7 +2187,6 @@ namespace Barotrauma.Networking
initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned: true }; initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned: true };
} }
List<Client> playingClients = new List<Client>(connectedClients); List<Client> playingClients = new List<Client>(connectedClients);
if (ServerSettings.AllowSpectating) if (ServerSettings.AllowSpectating)
{ {
@@ -2413,8 +2431,7 @@ namespace Barotrauma.Networking
mpCampaign.ClearSavedExperiencePoints(teamClients[i]); mpCampaign.ClearSavedExperiencePoints(teamClients[i]);
} }
spawnedCharacter.OwnerClientAddress = teamClients[i].Connection.Endpoint.Address; spawnedCharacter.SetOwnerClient(teamClients[i]);
spawnedCharacter.OwnerClientName = teamClients[i].Name;
} }
for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++)
@@ -2479,7 +2496,7 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Running; yield return CoroutineStatus.Running;
Voting?.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false);
GameMain.GameScreen.Select(); GameMain.GameScreen.Select();
@@ -2507,14 +2524,11 @@ namespace Barotrauma.Networking
private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize) private void SendStartMessage(int seed, string levelSeed, GameSession gameSession, Client client, bool includesFinalize)
{ {
MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign;
MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode;
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.STARTGAME); msg.WriteByte((byte)ServerPacketHeader.STARTGAME);
msg.WriteInt32(seed); msg.WriteInt32(seed);
msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier);
bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn);
msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn);
msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowDisguises);
msg.WriteBoolean(ServerSettings.AllowRewiring); msg.WriteBoolean(ServerSettings.AllowRewiring);
@@ -2530,7 +2544,7 @@ namespace Barotrauma.Networking
ServerSettings.WriteMonsterEnabled(msg); ServerSettings.WriteMonsterEnabled(msg);
if (campaign == null) if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign)
{ {
msg.WriteString(levelSeed); msg.WriteString(levelSeed);
msg.WriteSingle(ServerSettings.SelectedLevelDifficulty); msg.WriteSingle(ServerSettings.SelectedLevelDifficulty);
@@ -2566,6 +2580,23 @@ namespace Barotrauma.Networking
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
} }
private bool TrySendCampaignSetupInfo(Client client)
{
if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; }
const int MaxSaves = 255;
var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false);
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO);
msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves));
for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
{
msg.WriteNetSerializableStruct(saveInfos[i]);
}
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
return true;
}
private bool IsUsingRespawnShuttle() private bool IsUsingRespawnShuttle()
{ {
return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle); return ServerSettings.UseRespawnShuttle || (GameStarted && RespawnManager != null && RespawnManager.UsingShuttle);
@@ -3521,9 +3552,7 @@ namespace Barotrauma.Networking
//the client's previous character is no longer a remote player //the client's previous character is no longer a remote player
if (client.Character != null) if (client.Character != null)
{ {
client.Character.IsRemotePlayer = false; client.Character.SetOwnerClient(null);
client.Character.OwnerClientAddress = null;
client.Character.OwnerClientName = null;
} }
if (newCharacter == null) if (newCharacter == null)
@@ -3549,9 +3578,7 @@ namespace Barotrauma.Networking
newCharacter.Info.Character = newCharacter; newCharacter.Info.Character = newCharacter;
} }
newCharacter.OwnerClientAddress = client.Connection.Endpoint.Address; newCharacter.SetOwnerClient(client);
newCharacter.OwnerClientName = client.Name;
newCharacter.IsRemotePlayer = true;
newCharacter.Enabled = true; newCharacter.Enabled = true;
client.Character = newCharacter; client.Character = newCharacter;
CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); CreateEntityEvent(newCharacter, new Character.ControlEventData(client));
@@ -439,8 +439,7 @@ namespace Barotrauma.Networking
} }
clients[i].Character = character; clients[i].Character = character;
character.OwnerClientAddress = clients[i].Connection.Endpoint.Address; character.SetOwnerClient(clients[i]);
character.OwnerClientName = clients[i].Name;
GameServer.Log( GameServer.Log(
$"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning);
} }
@@ -117,13 +117,13 @@ namespace Barotrauma
public void StopSubmarineVote(bool passed) public void StopSubmarineVote(bool passed)
{ {
if (!(ActiveVote is SubmarineVote)) { return; } if (ActiveVote is not SubmarineVote) { return; }
StopActiveVote(passed); StopActiveVote(passed);
} }
public void StopMoneyTransferVote(bool passed) public void StopMoneyTransferVote(bool passed)
{ {
if (!(ActiveVote is TransferVote)) { return; } if (ActiveVote is not TransferVote) { return; }
StopActiveVote(passed); StopActiveVote(passed);
} }
@@ -155,7 +155,7 @@ namespace Barotrauma
GameMain.Server.UpdateVoteStatus(checkActiveVote: false); GameMain.Server.UpdateVoteStatus(checkActiveVote: false);
} }
private void StartOrEnqueueVote(IVote vote) private static void StartOrEnqueueVote(IVote vote)
{ {
if (ActiveVote == null) if (ActiveVote == null)
{ {
@@ -198,9 +198,9 @@ namespace Barotrauma
ActiveVote.Timer += deltaTime; ActiveVote.Timer += deltaTime;
if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame);
if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout || inGameClients.Count() == 1)
{ {
var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame);
var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter); var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter);
// Do not take unanswered into account for total // Do not take unanswered into account for total
@@ -216,7 +216,7 @@ namespace Barotrauma
} }
} }
public void ResetVotes(IEnumerable<Client> connectedClients, bool resetKickVotes) public static void ResetVotes(IEnumerable<Client> connectedClients, bool resetKickVotes)
{ {
foreach (Client client in connectedClients) foreach (Client client in connectedClients)
{ {
@@ -254,7 +254,14 @@ namespace Barotrauma
string modeIdentifier = inc.ReadString(); string modeIdentifier = inc.ReadString();
GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier);
if (mode == null || !mode.Votable) { break; } if (mode == null || !mode.Votable) { break; }
var prevHighestVoted = HighestVoted<GameModePreset>(VoteType.Mode, GameMain.Server.ConnectedClients);
sender.SetVote(voteType, mode); sender.SetVote(voteType, mode);
var newHighestVoted = HighestVoted<GameModePreset>(VoteType.Mode, GameMain.Server.ConnectedClients);
if (prevHighestVoted != newHighestVoted)
{
GameMain.NetLobbyScreen.SelectedModeIdentifier = mode.Identifier;
GameMain.NetLobbyScreen.LastUpdateID++;
}
break; break;
case VoteType.EndRound: case VoteType.EndRound:
if (!sender.HasSpawned) { return; } if (!sender.HasSpawned) { return; }
@@ -59,6 +59,7 @@ namespace Barotrauma
get { return GameModes[SelectedModeIndex].Identifier; } get { return GameModes[SelectedModeIndex].Identifier; }
set set
{ {
if (SelectedModeIdentifier == value) { return; }
for (int i = 0; i < GameModes.Length; i++) for (int i = 0; i < GameModes.Length; i++)
{ {
if (GameModes[i].Identifier == value) if (GameModes[i].Identifier == value)
@@ -127,9 +128,11 @@ namespace Barotrauma
{ {
LevelSeed = ToolBox.RandomSeed(8); LevelSeed = ToolBox.RandomSeed(8);
subs = SubmarineInfo.SavedSubmarines.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)).ToList(); subs = SubmarineInfo.SavedSubmarines
.Where(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus))
.ToList();
if (subs == null || subs.Count() == 0) if (subs == null || subs.Count == 0)
{ {
throw new Exception("No submarines are available."); throw new Exception("No submarines are available.");
} }
@@ -156,7 +159,7 @@ namespace Barotrauma
GameModes = GameModePreset.List.ToArray(); GameModes = GameModePreset.List.ToArray();
} }
private List<SubmarineInfo> subs; private readonly List<SubmarineInfo> subs;
public IReadOnlyList<SubmarineInfo> GetSubList() => subs; public IReadOnlyList<SubmarineInfo> GetSubList() => subs;
public string LevelSeed public string LevelSeed
@@ -192,7 +195,7 @@ namespace Barotrauma
public override void Select() public override void Select()
{ {
base.Select(); base.Select();
GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false);
if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this)
{ {
GameMain.GameSession = null; GameMain.GameSession = null;
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.20.15.0</Version> <Version>0.21.1.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -11,6 +11,7 @@
RadiationEnabled="false" RadiationEnabled="false"
StartingBalanceAmount="High" StartingBalanceAmount="High"
StartItemSet="easy" StartItemSet="easy"
MaxMissionCount="3"
Difficulty="Easy"/> Difficulty="Easy"/>
<CampaignSettings <CampaignSettings
presetname="Normal" presetname="Normal"
@@ -18,12 +19,14 @@
RadiationEnabled="false" RadiationEnabled="false"
StartingBalanceAmount="Medium" StartingBalanceAmount="Medium"
StartItemSet="normal" StartItemSet="normal"
MaxMissionCount="2"
Difficulty="Medium"/> Difficulty="Medium"/>
<CampaignSettings <CampaignSettings
presetname="Hard" presetname="Hard"
TutorialEnabled="false" TutorialEnabled="false"
RadiationEnabled="true" RadiationEnabled="false"
StartingBalanceAmount="Low" StartingBalanceAmount="Low"
StartItemSet="hard" StartItemSet="hard"
MaxMissionCount="1"
Difficulty="Hard"/> Difficulty="Hard"/>
</CampaignSettingPresets> </CampaignSettingPresets>
@@ -351,6 +351,21 @@ namespace Barotrauma
} }
} }
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public void ReequipUnequipped() public void ReequipUnequipped()
{ {
foreach (var item in unequippedItems) foreach (var item in unequippedItems)
@@ -355,6 +355,10 @@ namespace Barotrauma
{ {
targetingTag = "owner"; targetingTag = "owner";
} }
else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter))
{
targetingTag = "hostile";
}
else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP)) else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP))
{ {
targetingTag = tP.Tag; targetingTag = tP.Tag;
@@ -365,7 +369,7 @@ namespace Barotrauma
{ {
targetingTag = "husk"; targetingTag = "husk";
} }
else if (!Character.IsFriendly(targetCharacter)) else if (!Character.IsSameSpeciesOrGroup(targetCharacter))
{ {
if (enemy.CombatStrength > CombatStrength) if (enemy.CombatStrength > CombatStrength)
{ {
@@ -687,12 +691,9 @@ namespace Barotrauma
return a.Damage >= selectedTargetingParams.Threshold; return a.Damage >= selectedTargetingParams.Threshold;
} }
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker))
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
//TODO: a way for pets to differentiate hostile and friendly humans?
if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter))
{ {
// Attack the character that attacked the target we are protecting // Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group.
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget); SelectTarget(attacker.AiTarget);
State = AIState.Attack; State = AIState.Attack;
@@ -1514,9 +1514,18 @@ namespace Barotrauma
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f); startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
//do a raycast upwards to find any walls //do a raycast upwards to find any walls
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f; if (!Character.AnimController.TryGetCollider(0, out PhysicsBody mainCollider))
{
mainCollider = Character.AnimController.Collider;
}
float margin = 0.1f;
if (shouldCrouch)
{
margin *= 2;
}
float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null; shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null;
} }
public bool AllowCampaignInteraction() public bool AllowCampaignInteraction()
@@ -1589,7 +1598,27 @@ namespace Barotrauma
(!requireEquipped || character.HasEquippedItem(i)) && (!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, matchingItems); (predicate == null || predicate(i)), recursive, matchingItems);
items = matchingItems; items = matchingItems;
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); foreach (var item in matchingItems)
{
if (item == null) { continue; }
if (containedTag.IsEmpty || item.OwnInventory == null)
{
//no contained items required, this item's ok
return true;
}
var suitableSlot = item.GetComponent<ItemContainer>().FindSuitableSubContainerIndex(containedTag);
if (suitableSlot == null)
{
//no restrictions on the suitable slot
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage);
}
else
{
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value));
}
}
return false;
} }
public static void StructureDamaged(Structure structure, float damageAmount, Character character) public static void StructureDamaged(Structure structure, float damageAmount, Character character)
@@ -2016,11 +2045,9 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{ {
bool sameTeam = me.TeamID == other.TeamID; bool sameTeam = me.TeamID == other.TeamID;
bool friendlyTeam = IsOnFriendlyTeam(me, other); bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other);
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
if (!teamGood) { return false; } if (!teamGood) { return false; }
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); if (!me.IsSameSpeciesOrGroup(other)) { return false; }
if (!speciesGood) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{ {
var reputation = campaign.Map?.CurrentLocation?.Reputation; var reputation = campaign.Map?.CurrentLocation?.Reputation;
@@ -2029,30 +2056,14 @@ namespace Barotrauma
return false; return false;
} }
} }
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
return false;
}
return true; return true;
} }
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
switch (myTeam)
{
case CharacterTeamType.None:
case CharacterTeamType.Team1:
case CharacterTeamType.Team2:
// Only friendly to the same team and friendly NPCs
return otherTeam == CharacterTeamType.FriendlyNPC;
case CharacterTeamType.FriendlyNPC:
// Friendly NPCs are friendly to both teams
return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2;
default:
return true;
}
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate) public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
@@ -98,7 +98,7 @@ namespace Barotrauma
int containedItemCount = 0; int containedItemCount = 0;
foreach (Item it in container.Inventory.AllItems) foreach (Item it in container.Inventory.AllItems)
{ {
if (CheckItem(it)) if (CheckItem(it) && IsInTargetSlot(it))
{ {
containedItemCount++; containedItemCount++;
} }
@@ -1,5 +1,5 @@
using Barotrauma.Items.Components; using Barotrauma.Extensions;
using Barotrauma.Extensions; using Barotrauma.Items.Components;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -18,6 +18,7 @@ namespace Barotrauma
private AIObjectiveGetItem getDivingGear; private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen; private AIObjectiveContainItem getOxygen;
private Item targetItem; private Item targetItem;
private int? oxygenSourceSlotIndex;
public const float MIN_OXYGEN = 10; public const float MIN_OXYGEN = 10;
@@ -43,12 +44,15 @@ namespace Barotrauma
Abandon = true; Abandon = true;
return; return;
} }
targetItem = character.Inventory.FindItemByTag(gearTag, true);
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
if (targetItem == null && gearTag == LIGHT_DIVING_GEAR) if (targetItem == null && gearTag == LIGHT_DIVING_GEAR)
{ {
targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true); TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
} }
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{ {
TryAddSubObjective(ref getDivingGear, () => TryAddSubObjective(ref getDivingGear, () =>
{ {
@@ -84,7 +88,7 @@ namespace Barotrauma
else else
{ {
float min = GetMinOxygen(character); float min = GetMinOxygen(character);
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min)) if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it)))
{ {
TryAddSubObjective(ref getOxygen, () => TryAddSubObjective(ref getOxygen, () =>
{ {
@@ -93,7 +97,7 @@ namespace Barotrauma
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
{ {
character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f); character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1) if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1)
{ {
character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f); character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
} }
@@ -109,7 +113,8 @@ namespace Barotrauma
AllowToFindDivingGear = false, AllowToFindDivingGear = false,
AllowDangerousPressure = true, AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN, ConditionLevel = MIN_OXYGEN,
RemoveExistingWhenNecessary = true RemoveExistingWhenNecessary = true,
TargetSlot = oxygenSourceSlotIndex
}; };
if (container.HasSubContainers) if (container.HasSubContainers)
{ {
@@ -167,12 +172,36 @@ namespace Barotrauma
} }
} }
private bool IsSuitableContainedOxygenSource(Item item)
{
return
item != null &&
item.HasTag(OXYGEN_SOURCE) &&
item.Condition > 0 &&
(oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value));
}
private void TrySetTargetItem(Item item)
{
if (targetItem == item) { return; }
targetItem = item;
if (targetItem != null)
{
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
}
else
{
oxygenSourceSlotIndex = null;
}
}
public override void Reset() public override void Reset()
{ {
base.Reset(); base.Reset();
getDivingGear = null; getDivingGear = null;
getOxygen = null; getOxygen = null;
targetItem = null; targetItem = null;
oxygenSourceSlotIndex = null;
} }
public static float GetMinOxygen(Character character) public static float GetMinOxygen(Character character)
@@ -71,7 +71,7 @@ namespace Barotrauma
Priority = 100; Priority = 100;
} }
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) && else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
{ {
// Ordered to follow, hold position, or return back to main sub inside a hostile sub // Ordered to follow, hold position, or return back to main sub inside a hostile sub
// -> ignore find safety unless we need to find a diving gear // -> ignore find safety unless we need to find a diving gear
@@ -413,7 +413,7 @@ namespace Barotrauma
} }
} }
} }
private void ApplyTreatment(Affliction affliction, Item item) private void ApplyTreatment(Affliction affliction, Item item)
{ {
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
@@ -371,7 +371,8 @@ namespace Barotrauma
private int CalculateCellCount(int minValue, int maxValue) private int CalculateCellCount(int minValue, int maxValue)
{ {
if (maxValue == 0) { return 0; } if (maxValue == 0) { return 0; }
float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier); float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier);
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t)); return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
} }
@@ -381,7 +382,8 @@ namespace Barotrauma
float delay = Config.AgentSpawnDelay; float delay = Config.AgentSpawnDelay;
float min = delay; float min = delay;
float max = delay * 6; float max = delay * 6;
float t = Level.Loaded.Difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor); float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t)); return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
} }
@@ -24,7 +24,7 @@ namespace Barotrauma
public bool IsAiming => wasAiming; public bool IsAiming => wasAiming;
public bool IsAimingMelee => wasAimingMelee; public bool IsAimingMelee => wasAimingMelee;
protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim); protected bool Aiming => aiming || aimingMelee || FlipLockTime > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
public float ArmLength => upperArmLength + forearmLength; public float ArmLength => upperArmLength + forearmLength;
@@ -278,7 +278,11 @@ namespace Barotrauma
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f; public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
public float LockFlippingUntil; public float FlipLockTime { get; private set; }
public void LockFlipping(float time = 0.2f)
{
FlipLockTime = (float)Timing.TotalTime + time;
}
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{ {
@@ -994,7 +994,7 @@ namespace Barotrauma
foreach (Limb l in Limbs) foreach (Limb l in Limbs)
{ {
if (l.IsSevered) { continue; } if (l.IsSevered) { continue; }
if (!l.DoesFlip) { continue; } if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal) if (RagdollParams.IsSpritesheetOrientationHorizontal)
{ {
//horizontally aligned limbs need to be flipped 180 degrees //horizontally aligned limbs need to be flipped 180 degrees
@@ -1014,7 +1014,7 @@ namespace Barotrauma
if (l.IsSevered) { continue; } if (l.IsSevered) { continue; }
float rotation = l.body.Rotation; float rotation = l.body.Rotation;
if (l.DoesFlip) if (l.DoesMirror)
{ {
if (RagdollParams.IsSpritesheetOrientationHorizontal) if (RagdollParams.IsSpritesheetOrientationHorizontal)
{ {
@@ -150,8 +150,10 @@ namespace Barotrauma
private readonly float movementLerp; private readonly float movementLerp;
private float cprAnimTimer; private float cprAnimTimer,cprPump;
private float cprPump;
private float fallingProneAnimTimer;
const float FallingProneAnimDuration = 1.0f;
private bool swimming; private bool swimming;
//time until the character can switch from walking to swimming or vice versa //time until the character can switch from walking to swimming or vice versa
@@ -268,7 +270,8 @@ namespace Barotrauma
if (deathAnimTimer < deathAnimDuration) if (deathAnimTimer < deathAnimDuration)
{ {
deathAnimTimer += deltaTime; deathAnimTimer += deltaTime;
UpdateDying(deltaTime); //the force/torque used to move the limbs goes from 1 to 0 during the death anim duration
UpdateFallingProne(1.0f - deathAnimTimer / deathAnimDuration);
} }
} }
else else
@@ -278,6 +281,11 @@ namespace Barotrauma
if (!character.CanMove) if (!character.CanMove)
{ {
if (fallingProneAnimTimer < FallingProneAnimDuration)
{
fallingProneAnimTimer += deltaTime;
UpdateFallingProne(1.0f);
}
levitatingCollider = false; levitatingCollider = false;
Collider.FarseerBody.FixedRotation = false; Collider.FarseerBody.FixedRotation = false;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
@@ -291,18 +299,20 @@ namespace Barotrauma
} }
return; return;
} }
fallingProneAnimTimer = 0.0f;
//re-enable collider //re-enable collider
if (!Collider.Enabled) if (!Collider.Enabled)
{ {
var lowestLimb = FindLowestLimb(); var lowestLimb = FindLowestLimb();
Collider.SetTransform(new Vector2( Collider.SetTransform(new Vector2(
Collider.SimPosition.X, Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
Collider.Rotation); Collider.Rotation);
Collider.FarseerBody.ResetDynamics(); Collider.FarseerBody.ResetDynamics();
Collider.FarseerBody.LinearVelocity = MainLimb.LinearVelocity;
Collider.Enabled = true; Collider.Enabled = true;
} }
@@ -431,7 +441,7 @@ namespace Barotrauma
} }
} }
if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck) if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck)
{ {
Flip(); Flip();
} }
@@ -1292,10 +1302,9 @@ namespace Barotrauma
} }
} }
void UpdateDying(float deltaTime) void UpdateFallingProne(float strength)
{ {
//the force/torque used to move the limbs goes from 1 to 0 during the death anim duration if (strength <= 0.0f) { return; }
float strength = 1.0f - deathAnimTimer / deathAnimDuration;
Limb head = GetLimb(LimbType.Head); Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso); Limb torso = GetLimb(LimbType.Torso);
@@ -1319,6 +1328,19 @@ namespace Barotrauma
} }
if (torso == null) { return; } if (torso == null) { return; }
//make the torso tip over
//otherwise it tends to just drop straight down, pinning the characters legs in a weird pose
if (!InWater)
{
//prefer tipping over in the same direction the torso is rotating
//or moving
//or lastly, in the direction it's facing if it's not moving/rotating
float fallDirection = Math.Sign(torso.body.AngularVelocity - torso.body.LinearVelocity.X - Dir * 0.01f);
float torque = MathF.Cos(torso.Rotation) * fallDirection * 5.0f * strength;
torso.body.ApplyTorque(torque * torso.body.Mass);
}
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
{ {
@@ -1503,12 +1525,12 @@ namespace Barotrauma
Limb rightHand = GetLimb(LimbType.RightHand); Limb rightHand = GetLimb(LimbType.RightHand);
Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm);
if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb; if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; }
Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm);
if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso); if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb; if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; }
if (!target.AllowInput) if (!target.AllowInput)
{ {
@@ -1644,18 +1666,24 @@ namespace Barotrauma
pullLimb.PullJointEnabled = true; pullLimb.PullJointEnabled = true;
if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb)
{ {
Vector2 pullLimbAnchor = targetLimb.SimPosition;
pullLimb.PullJointMaxForce = 5000.0f; pullLimb.PullJointMaxForce = 5000.0f;
if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging))
{ {
targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f);
} }
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; }
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength); Vector2 shoulderPos = rightShoulder.WorldAnchorA;
float targetDist = Vector2.Distance(targetLimb.SimPosition, shoulderPos);
Vector2 dragDir = (targetLimb.SimPosition - shoulderPos) / targetDist;
if (!MathUtils.IsValid(dragDir)) { dragDir = -Vector2.UnitY; }
if (!InWater)
{
//lerp the arm downwards when not swimming
dragDir = Vector2.Lerp(dragDir, -Vector2.UnitY, 0.5f);
}
Vector2 pullLimbAnchor = shoulderPos + dragDir * Math.Min(targetDist, (upperArmLength + forearmLength) * 2);
targetAnchor = shoulderPos + dragDir * (upperArmLength + forearmLength);
targetForce = 200.0f; targetForce = 200.0f;
if (target.Submarine != character.Submarine) if (target.Submarine != character.Submarine)
{ {
@@ -1723,7 +1751,7 @@ namespace Barotrauma
{ {
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X) if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
{ {
target.AnimController.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; target.AnimController.LockFlipping(0.5f);
} }
else else
{ {
@@ -1822,16 +1850,22 @@ namespace Barotrauma
public override void Flip() public override void Flip()
{ {
if (Character == null || Character.Removed)
{
LogAccessedRemovedCharacterError();
return;
}
base.Flip(); base.Flip();
WalkPos = -WalkPos; WalkPos = -WalkPos;
Limb torso = GetLimb(LimbType.Torso); Limb torso = GetLimb(LimbType.Torso);
if (torso == null) { return; }
Vector2 difference;
Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation); Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation);
Vector2 difference;
foreach (Item heldItem in character.HeldItems) foreach (Item heldItem in character.HeldItems)
{ {
if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent<Holdable>() != null) if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent<Holdable>() != null)
@@ -57,17 +57,7 @@ namespace Barotrauma
{ {
if (limbs == null) if (limbs == null)
{ {
if (!accessRemovedCharacterErrorShown) LogAccessedRemovedCharacterError();
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
return Array.Empty<Limb>(); return Array.Empty<Limb>();
} }
return limbs; return limbs;
@@ -158,6 +148,20 @@ namespace Barotrauma
} }
} }
public bool TryGetCollider(int index, out PhysicsBody collider)
{
collider = null;
try
{
collider = this.collider?[index];
return true;
}
catch
{
return false;
}
}
public int ColliderIndex public int ColliderIndex
{ {
get get
@@ -873,7 +877,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs) foreach (Limb limb in Limbs)
{ {
if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; } if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; }
limb.Dir = Dir; limb.Dir = Dir;
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
limb.MirrorPullJoint(); limb.MirrorPullJoint();
@@ -1428,6 +1432,21 @@ namespace Barotrauma
return true; return true;
} }
protected void LogAccessedRemovedCharacterError()
{
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll:AccessRemoved",
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
}
partial void UpdateProjSpecific(float deltaTime, Camera cam); partial void UpdateProjSpecific(float deltaTime, Camera cam);
partial void Splash(Limb limb, Hull limbHull); partial void Splash(Limb limb, Hull limbHull);
@@ -492,37 +492,52 @@ namespace Barotrauma
DamageParticles(deltaTime, worldPosition); DamageParticles(deltaTime, worldPosition);
var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult(); var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult();
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
var additionalEffectType = ActionType.OnUse;
if (targetCharacter != null && targetCharacter.IsDead) if (targetCharacter != null && targetCharacter.IsDead)
{ {
effectType = ActionType.OnEating; additionalEffectType = ActionType.OnEating;
} }
foreach (StatusEffect effect in statusEffects) foreach (StatusEffect effect in statusEffects)
{ {
effect.sourceBody = sourceBody; effect.sourceBody = sourceBody;
if (effect.HasTargetType(StatusEffect.TargetType.This)) if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
{ {
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied var t = sourceLimb ?? attacker as ISerializableEntity;
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition); if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, attacker, t, worldPosition);
}
effect.Apply(additionalEffectType, deltaTime, attacker, t, worldPosition);
} }
if (effect.HasTargetType(StatusEffect.TargetType.Parent)) if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{ {
effect.Apply(effectType, deltaTime, attacker, attacker); if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
}
effect.Apply(additionalEffectType, deltaTime, attacker, attacker);
} }
if (targetCharacter != null) if (targetCharacter != null)
{ {
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb)) if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{ {
effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb); if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
}
effect.Apply(additionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
} }
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{ {
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList()); // TODO: do we need the conversion to list here? It generates garbage.
var targets = targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets);
}
effect.Apply(additionalEffectType, deltaTime, targetCharacter, targets);
} }
} }
if (target is Entity targetEntity) if (target is Entity targetEntity)
@@ -532,18 +547,30 @@ namespace Barotrauma
{ {
targets.Clear(); targets.Clear();
effect.AddNearbyTargets(worldPosition, targets); effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetEntity, targets); if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targets);
}
effect.Apply(additionalEffectType, deltaTime, targetEntity, targets);
} }
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{ {
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition); if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
}
effect.Apply(additionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
} }
} }
if (effect.HasTargetType(StatusEffect.TargetType.Contained)) if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{ {
targets.Clear(); targets.Clear();
targets.AddRange(attacker.Inventory.AllItems); targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets); if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
}
effect.Apply(additionalEffectType, deltaTime, attacker, targets);
} }
} }
@@ -579,47 +606,52 @@ namespace Barotrauma
} }
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects) foreach (StatusEffect effect in statusEffects)
{ {
effect.sourceBody = sourceBody; effect.sourceBody = sourceBody;
if (effect.HasTargetType(StatusEffect.TargetType.This)) if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
{ {
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); effect.Apply(conditionalEffectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
effect.Apply(ActionType.OnUse, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
} }
if (effect.HasTargetType(StatusEffect.TargetType.Parent)) if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{ {
effect.Apply(effectType, deltaTime, attacker, attacker); effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
effect.Apply(ActionType.OnUse, deltaTime, attacker, attacker);
} }
if (effect.HasTargetType(StatusEffect.TargetType.Character)) if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{ {
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character); effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb.character);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb.character);
} }
if (effect.HasTargetType(StatusEffect.TargetType.Limb)) if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{ {
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb); effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb);
} }
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{ {
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList()); // TODO: do we need the conversion to list here? It generates garbage.
var targets = targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
} }
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{ {
targets.Clear(); targets.Clear();
effect.AddNearbyTargets(worldPosition, targets); effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetLimb.character, targets); effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
} effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
} }
if (effect.HasTargetType(StatusEffect.TargetType.Contained)) if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{ {
targets.Clear(); targets.Clear();
targets.AddRange(attacker.Inventory.AllItems); targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets); effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
effect.Apply(ActionType.OnUse, deltaTime, attacker, targets);
} }
} }
@@ -31,6 +31,9 @@ namespace Barotrauma
{ {
public readonly static List<Character> CharacterList = new List<Character>(); public readonly static List<Character> CharacterList = new List<Character>();
public const float MaxHighlightDistance = 150.0f;
public const float MaxDragDistance = 200.0f;
partial void UpdateLimbLightSource(Limb limb); partial void UpdateLimbLightSource(Limb limb);
private bool enabled = true; private bool enabled = true;
@@ -354,9 +357,15 @@ namespace Barotrauma
public readonly CharacterPrefab Prefab; public readonly CharacterPrefab Prefab;
public readonly CharacterParams Params; public readonly CharacterParams Params;
public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
public Identifier Group => Params.Group; public Identifier Group => Params.Group;
public bool IsHumanoid => Params.Humanoid; public bool IsHumanoid => Params.Humanoid;
public bool IsMachine => Params.IsMachine;
public bool IsHusk => Params.Husk; public bool IsHusk => Params.Husk;
public bool IsMale => info?.IsMale ?? false; public bool IsMale => info?.IsMale ?? false;
@@ -805,6 +814,11 @@ namespace Barotrauma
public float HealthPercentage => CharacterHealth.HealthPercentage; public float HealthPercentage => CharacterHealth.HealthPercentage;
public float MaxVitality => CharacterHealth.MaxVitality; public float MaxVitality => CharacterHealth.MaxVitality;
public float MaxHealth => MaxVitality; public float MaxHealth => MaxVitality;
/// <summary>
/// Was the character in full health at the beginning of the frame?
/// </summary>
public bool WasFullHealth => CharacterHealth.WasInFullHealth;
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
public float EmpVulnerability => Params.Health.EmpVulnerability; public float EmpVulnerability => Params.Health.EmpVulnerability;
@@ -1741,7 +1755,7 @@ namespace Barotrauma
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed); float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed); targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed); targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier); SpeedMultiplier = Math.Max(0.0f, greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier));
targetMovement *= SpeedMultiplier; targetMovement *= SpeedMultiplier;
// Reset, status effects will set the value before the next update // Reset, status effects will set the value before the next update
ResetSpeedMultiplier(); ResetSpeedMultiplier();
@@ -2201,7 +2215,9 @@ namespace Barotrauma
if (SelectedCharacter != null) if (SelectedCharacter != null)
{ {
if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected) if (!SelectedCharacter.CanBeSelected ||
(Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance &&
SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance)))
{ {
DeselectCharacter(); DeselectCharacter();
} }
@@ -2494,8 +2510,12 @@ namespace Barotrauma
if (!skipDistanceCheck) if (!skipDistanceCheck)
{ {
maxDist = ConvertUnits.ToSimUnits(maxDist); maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent());
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; } if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist &&
Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist)
{
return false;
}
} }
return checkVisibility ? CanSeeCharacter(c) : true; return checkVisibility ? CanSeeCharacter(c) : true;
@@ -3138,56 +3158,57 @@ namespace Barotrauma
UpdateAIChatMessages(deltaTime); UpdateAIChatMessages(deltaTime);
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true)
bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f;
bool wasRagdolled = false;
bool selfRagdolled = false;
if (IsForceRagdolled)
{ {
IsRagdolled = IsForceRagdolled; bool wasRagdolled = IsRagdolled;
} if (IsForceRagdolled)
else if (this != Controlled)
{
wasRagdolled = IsRagdolled;
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll);
}
//Keep us ragdolled if we were forced or we're too speedy to unragdoll
else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll))
{
if (ragdollingLockTimer > 0.0f)
{ {
SetInput(InputType.Ragdoll, false, true); IsRagdolled = IsForceRagdolled;
ragdollingLockTimer -= deltaTime; }
else if (this != Controlled)
{
wasRagdolled = IsRagdolled;
IsRagdolled = IsKeyDown(InputType.Ragdoll);
} }
else else
{ {
wasRagdolled = IsRagdolled; bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body);
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves bool bodyMovingTooFast(PhysicsBody body)
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; } {
return
body.LinearVelocity.LengthSquared() > 8.0f * 8.0f ||
//falling down counts as going too fast
(!InWater && body.LinearVelocity.Y < -5.0f);
}
if (ragdollingLockTimer > 0.0f)
{
ragdollingLockTimer -= deltaTime;
}
else if (!tooFastToUnragdoll)
{
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; }
}
if (IsRagdolled)
{
SetInput(InputType.Ragdoll, false, true);
}
} }
} if (!wasRagdolled && IsRagdolled)
if (!wasRagdolled && IsRagdolled)
{
if (selfRagdolled)
{ {
CheckTalents(AbilityEffectType.OnSelfRagdoll); CheckTalents(AbilityEffectType.OnRagdoll);
} }
// currently does not work when you are stunned, like it should
CheckTalents(AbilityEffectType.OnRagdoll);
} }
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
//ragdoll button
if (IsRagdolled || !CanMove) if (IsRagdolled || !CanMove)
{ {
if (AnimController is HumanoidAnimController humanAnimController) if (AnimController is HumanoidAnimController humanAnimController)
{ {
humanAnimController.Crouching = false; humanAnimController.Crouching = false;
} }
if (IsRagdolled) { AnimController.IgnorePlatforms = true; }
AnimController.ResetPullJoints(); AnimController.ResetPullJoints();
SelectedItem = SelectedSecondaryItem = null; SelectedItem = SelectedSecondaryItem = null;
return; return;
@@ -3365,6 +3386,20 @@ namespace Barotrauma
return distSqr; return distSqr;
} }
public float GetDistanceToClosestLimb(Vector2 simPos)
{
float closestDist = float.MaxValue;
foreach (Limb limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
float dist = Vector2.Distance(simPos, limb.SimPosition);
dist -= limb.body.GetMaxExtent();
closestDist = Math.Min(closestDist, dist);
if (closestDist <= 0.0f) { return 0.0f; }
}
return closestDist;
}
private float despawnTimer; private float despawnTimer;
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true) private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true)
{ {
@@ -3885,8 +3920,8 @@ namespace Barotrauma
{ {
foreach (Affliction affliction in attackResult.Afflictions) foreach (Affliction affliction in attackResult.Afflictions)
{ {
if (affliction.Strength == 0.0f) continue; if (Math.Abs(affliction.Strength) <= 0.1f) { continue;}
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}"); sb.Append($" {affliction.Prefab.Name}: {affliction.Strength.ToString("0.0")}");
} }
} }
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack); GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
@@ -4484,7 +4519,10 @@ namespace Barotrauma
#if CLIENT #if CLIENT
//ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns
Inventory?.ApplyReceivedState(); if (GameMain.Client?.ClientPeer is { IsActive: true })
{
Inventory?.ApplyReceivedState();
}
#endif #endif
base.Remove(); base.Remove();
@@ -5200,15 +5238,13 @@ namespace Barotrauma
public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier); public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier);
/// <summary>
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary>
public bool IsFriendly(Character other) => IsFriendly(this, other); public bool IsFriendly(Character other) => IsFriendly(this, other);
/// <summary> public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary> public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
public void StopClimbing() public void StopClimbing()
{ {
@@ -778,9 +778,10 @@ namespace Barotrauma
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White); //default to transparent color, it's invalid and will be replaced with a random one in CheckColors
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White); Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent);
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent);
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent);
CheckColors(); CheckColors();
TryLoadNameAndTitle(npcIdentifier); TryLoadNameAndTitle(npcIdentifier);
@@ -602,6 +602,18 @@ namespace Barotrauma
break; break;
} }
} }
for (int i = 0; i < effects.Count; i++)
{
for (int j = i + 1; j < effects.Count; j++)
{
var a = effects[i];
var b = effects[j];
if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength)
{
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.");
}
}
}
} }
public void ClearEffects() public void ClearEffects()
@@ -230,6 +230,11 @@ namespace Barotrauma
public float StunTimer { get; private set; } public float StunTimer { get; private set; }
/// <summary>
/// Was the character in full health at the beginning of the frame?
/// </summary>
public bool WasInFullHealth { get; private set; }
public Affliction PressureAffliction public Affliction PressureAffliction
{ {
get { return pressureAffliction; } get { return pressureAffliction; }
@@ -772,6 +777,8 @@ namespace Barotrauma
public void Update(float deltaTime) public void Update(float deltaTime)
{ {
WasInFullHealth = vitality >= MaxVitality;
UpdateOxygen(deltaTime); UpdateOxygen(deltaTime);
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
@@ -878,7 +885,11 @@ namespace Barotrauma
private void UpdateOxygen(float deltaTime) private void UpdateOxygen(float deltaTime)
{ {
if (!Character.NeedsOxygen) { return; } if (!Character.NeedsOxygen)
{
oxygenLowAffliction.Strength = 0.0f;
return;
}
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
float prevOxygen = OxygenAmount; float prevOxygen = OxygenAmount;
@@ -980,6 +991,8 @@ namespace Barotrauma
UpdateLimbAfflictionOverlays(); UpdateLimbAfflictionOverlays();
UpdateSkinTint(); UpdateSkinTint();
Character.Kill(type, affliction); Character.Kill(type, affliction);
WasInFullHealth = false;
#if CLIENT #if CLIENT
DisplayVitalityDelay = 0.0f; DisplayVitalityDelay = 0.0f;
DisplayedVitality = Vitality; DisplayedVitality = Vitality;
@@ -1024,17 +1037,18 @@ namespace Barotrauma
} }
private readonly List<Affliction> allAfflictions = new List<Affliction>(); private readonly List<Affliction> allAfflictions = new List<Affliction>();
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions) private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{ {
allAfflictions.Clear(); allAfflictions.Clear();
if (!mergeSameAfflictions) if (!mergeSameAfflictions)
{ {
allAfflictions.AddRange(afflictions.Keys); allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
} }
else else
{ {
foreach (Affliction affliction in afflictions.Keys) foreach (Affliction affliction in afflictions.Keys)
{ {
if (predicate != null && !predicate(affliction)) { continue; }
var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab); var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
if (existingAffliction == null) if (existingAffliction == null)
{ {
@@ -200,7 +200,7 @@ namespace Barotrauma
} }
} }
} }
partial class Limb : ISerializableEntity, ISpatialEntity partial class Limb : ISerializableEntity, ISpatialEntity
{ {
//how long it takes for severed limbs to fade out //how long it takes for severed limbs to fade out
@@ -215,7 +215,7 @@ namespace Barotrauma
//the physics body of the limb //the physics body of the limb
public PhysicsBody body; public PhysicsBody body;
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
public Hull Hull; public Hull Hull;
@@ -249,7 +249,7 @@ namespace Barotrauma
} }
} }
} }
private bool isSevered; private bool isSevered;
private float severedFadeOutTimer; private float severedFadeOutTimer;
@@ -269,7 +269,7 @@ namespace Barotrauma
mouthPos = value; mouthPos = value;
} }
} }
public readonly Attack attack; public readonly Attack attack;
public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>(); public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>();
@@ -282,39 +282,73 @@ namespace Barotrauma
{ {
get get
{ {
if (character.AnimController.CurrentAnimationParams is GroundedMovementParams) if (character?.AnimController.CurrentAnimationParams is GroundedMovementParams && IsLeg)
{ {
switch (type) // Legs always has to flip when not swimming
{ return true;
case LimbType.LeftFoot:
case LimbType.LeftLeg:
case LimbType.LeftThigh:
case LimbType.RightFoot:
case LimbType.RightLeg:
case LimbType.RightThigh:
// Legs always has to flip
return true;
}
} }
return Params.Flip; return Params.Flip;
} }
} }
public bool DoesMirror
{
get
{
if (IsLeg)
{
// Legs always has to mirror
return true;
}
return DoesFlip;
}
}
public float SteerForce => Params.SteerForce; public float SteerForce => Params.SteerForce;
public Vector2 DebugTargetPos; public Vector2 DebugTargetPos;
public Vector2 DebugRefPos; public Vector2 DebugRefPos;
public bool IsLowerBody => public bool IsLowerBody
type == LimbType.LeftLeg || {
type == LimbType.RightLeg || get
type == LimbType.LeftFoot || {
type == LimbType.RightFoot || switch (type)
type == LimbType.Tail || {
type == LimbType.Legs || case LimbType.LeftLeg:
type == LimbType.RightThigh || case LimbType.RightLeg:
type == LimbType.LeftThigh || case LimbType.LeftFoot:
type == LimbType.Waist; case LimbType.RightFoot:
case LimbType.Tail:
case LimbType.Legs:
case LimbType.LeftThigh:
case LimbType.RightThigh:
case LimbType.Waist:
return true;
default:
return false;
}
}
}
public bool IsLeg
{
get
{
switch (type)
{
case LimbType.LeftFoot:
case LimbType.LeftLeg:
case LimbType.LeftThigh:
case LimbType.RightFoot:
case LimbType.RightLeg:
case LimbType.RightThigh:
return true;
default:
return false;
}
}
}
public bool IsSevered public bool IsSevered
{ {
@@ -75,13 +75,14 @@ namespace Barotrauma.Abilities
if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; } if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; }
switch (wt) switch (wt)
{ {
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee: case WeaponType.Melee:
//if the item has an active projectile component (has been fired), don't consider it a melee weapon
if (item?.GetComponent<Projectile>() is { IsActive: true }) { continue; }
if (item?.GetComponent<MeleeWeapon>() != null) { return true; } if (item?.GetComponent<MeleeWeapon>() != null) { return true; }
break; break;
case WeaponType.Ranged: case WeaponType.Ranged:
//if the item has a melee weapon component that's being used now, don't consider it a projectile
if (item?.GetComponent<MeleeWeapon>() is { Hitting: true }) { continue; }
if (item?.GetComponent<Projectile>() != null) { return true; } if (item?.GetComponent<Projectile>() != null) { return true; }
break; break;
case WeaponType.HandheldRanged: case WeaponType.HandheldRanged:
@@ -28,7 +28,6 @@ namespace Barotrauma.Abilities
conditionals.Add(new PropertyConditional(attribute)); conditionals.Add(new PropertyConditional(attribute));
} }
} }
break;
} }
} }
@@ -37,19 +37,7 @@ namespace Barotrauma.Abilities
if (itemPrefab != null) if (itemPrefab != null)
{ {
if (category != MapEntityCategory.None) return MatchesItem(itemPrefab);
{
if (!itemPrefab.Category.HasFlag(category)) { return false; }
}
if (identifiers.Any())
{
if (!identifiers.Any(t => itemPrefab.Identifier == t))
{
return false;
}
}
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
} }
else else
{ {
@@ -57,5 +45,22 @@ namespace Barotrauma.Abilities
return false; return false;
} }
} }
public bool MatchesItem(ItemPrefab itemPrefab)
{
if (category != MapEntityCategory.None)
{
if (!itemPrefab.Category.HasFlag(category)) { return false; }
}
if (identifiers.Any())
{
if (!identifiers.Any(t => itemPrefab.Identifier == t))
{
return false;
}
}
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
}
} }
} }
@@ -17,6 +17,14 @@ namespace Barotrauma.Abilities
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty); tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
} }
public override void InitializeAbility(bool addingFirstTime)
{
if (addingFirstTime)
{
VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f);
}
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{ {
if (conditionsMatched) if (conditionsMatched)
@@ -13,6 +13,11 @@
value = abilityElement.GetAttributeFloat("value", 0f); value = abilityElement.GetAttributeFloat("value", 0f);
} }
public override void InitializeAbility(bool addingFirstTime)
{
VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{ {
if (conditionsMatched != lastState) if (conditionsMatched != lastState)
@@ -1,19 +1,35 @@
#nullable enable #nullable enable
using Barotrauma.Extensions;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility
{ {
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } private readonly AbilityConditionItem? condition;
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
var conditionElement = abilityElement.GetChildElement(nameof(AbilityConditionItem));
if (conditionElement != null)
{
condition = new AbilityConditionItem(CharacterTalent, conditionElement);
}
}
protected override void ApplyEffect(AbilityObject abilityObject) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; } if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; }
int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced); List<Item> applicableIngredients = condition == null ?
ingredients.Items.RemoveAt(randomIndex); ingredients.Items.ToList() :
ingredients.Items.Where(it => condition.MatchesItem(it.Prefab)).ToList();
if (applicableIngredients.None()) { return; }
ingredients.Items.Remove(applicableIngredients.GetRandom(Rand.RandSync.Unsynced));
} }
} }
} }
@@ -288,9 +288,7 @@ namespace Barotrauma
if (errorCatcher.Errors.Any()) if (errorCatcher.Errors.Any())
{ {
yield return ContentPackageManager.LoadProgress.Failure( yield return ContentPackageManager.LoadProgress.Failure(errorCatcher.Errors.Select(e => e.Text));
ContentPackageManager.LoadProgress.Error
.Reason.ConsoleErrorsThrown);
yield break; yield break;
} }
yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length); yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length);
@@ -23,6 +23,8 @@ namespace Barotrauma
public const string RegularPackagesElementName = "regularpackages"; public const string RegularPackagesElementName = "regularpackages";
public const string RegularPackagesSubElementName = "package"; public const string RegularPackagesSubElementName = "package";
public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent);
public static class EnabledPackages public static class EnabledPackages
{ {
public static CorePackage? Core { get; private set; } = null; public static CorePackage? Core { get; private set; } = null;
@@ -435,22 +437,19 @@ namespace Barotrauma
public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result) public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
{ {
public readonly record struct Error( public readonly record struct Error(
Error.Reason ErrorReason, Either<ImmutableArray<string>, Exception> ErrorsOrException)
Option<Exception> Exception)
{ {
public enum Reason { Exception, ConsoleErrorsThrown } public Error(IEnumerable<string> errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { }
public Error(Exception exception) : this(ErrorsOrException: exception) { }
public Error(Reason reason) : this(reason, Option.None) { }
public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { }
} }
public static LoadProgress Failure(Exception exception) public static LoadProgress Failure(Exception exception)
=> new LoadProgress( => new LoadProgress(
Result<float, Error>.Failure(new Error(exception))); Result<float, Error>.Failure(new Error(exception)));
public static LoadProgress Failure(Error.Reason reason) public static LoadProgress Failure(IEnumerable<string> errorMessages)
=> new LoadProgress( => new LoadProgress(
Result<float, Error>.Failure(new Error(reason))); Result<float, Error>.Failure(new Error(errorMessages)));
public static LoadProgress Progress(float value) public static LoadProgress Progress(float value)
=> new LoadProgress( => new LoadProgress(
@@ -88,6 +88,7 @@ namespace Barotrauma
public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def);
public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def);
public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def);
public Range<int> GetAttributeRange(string key, in Range<int> def) => Element.GetAttributeRange(key, def);
public Identifier VariantOf() => Element.VariantOf(); public Identifier VariantOf() => Element.VariantOf();
@@ -1312,10 +1312,10 @@ namespace Barotrauma
} }
}, () => }, () =>
{ {
return new[] return new[]
{ {
FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray(), FactionPrefab.Prefabs.Select(static f => f.Identifier.Value).ToArray(),
GameMain.GameSession?.Campaign.Factions.Select(f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>() GameMain.GameSession?.Campaign?.Factions.Select(static f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
}; };
}, true)); }, true));
@@ -49,7 +49,6 @@ namespace Barotrauma
OnUseRangedWeapon, OnUseRangedWeapon,
OnReduceAffliction, OnReduceAffliction,
OnAddDamageAffliction, OnAddDamageAffliction,
OnSelfRagdoll,
OnRagdoll, OnRagdoll,
OnRoundEnd, OnRoundEnd,
OnLootCharacter, OnLootCharacter,
@@ -168,7 +167,7 @@ namespace Barotrauma
PumpSpeed, PumpSpeed,
PumpMaxFlow, PumpMaxFlow,
ReactorMaxOutput, ReactorMaxOutput,
ReactorFuelEfficiency, ReactorFuelConsumption,
DeconstructorSpeed, DeconstructorSpeed,
FabricationSpeed FabricationSpeed
} }
@@ -226,7 +226,7 @@ namespace Barotrauma
bool requiresRescue = element.GetAttributeBool("requirerescue", false); bool requiresRescue = element.GetAttributeBool("requirerescue", false);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true); Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos);
if (spawnPos is WayPoint wp) if (spawnPos is WayPoint wp)
{ {
@@ -97,7 +97,7 @@ namespace Barotrauma
List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs(); List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
{ {
if (!connectedSubs.Contains(item.Submarine)) { continue; } if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; }
if (item.GetComponent<PowerTransfer>() != null || if (item.GetComponent<PowerTransfer>() != null ||
item.GetComponent<PowerContainer>() != null || item.GetComponent<PowerContainer>() != null ||
item.GetComponent<Reactor>() != null || item.GetComponent<Reactor>() != null ||
@@ -404,7 +404,7 @@ namespace Barotrauma
{ {
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value)); info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value));
} }
// apply money gains afterwards to prevent them from affecting XP gains // apply money gains afterwards to prevent them from affecting XP gains
@@ -545,17 +545,14 @@ namespace Barotrauma
return humanPrefab; return humanPrefab;
} }
protected Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true) protected static Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient)
{ {
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
characterInfo.TeamID = teamType; characterInfo.TeamID = teamType;
if (positionToStayIn == null) positionToStayIn ??=
{
positionToStayIn =
WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ?? WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
WayPoint.GetRandom(SpawnType.Human, null, submarine); WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.HumanPrefab = humanPrefab; spawnedCharacter.HumanPrefab = humanPrefab;
@@ -201,7 +201,7 @@ namespace Barotrauma
{ {
continue; continue;
} }
if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2()))) if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2()))
{ {
removals.Add(position); removals.Add(position);
} }
@@ -371,7 +371,7 @@ namespace Barotrauma
if (!SendUserStatistics) { return; } if (!SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent)) if (ContentPackageManager.ModsEnabled)
{ {
message = "[MODDED] " + message; message = "[MODDED] " + message;
} }
@@ -82,7 +82,7 @@ namespace Barotrauma
} }
public const int DefaultMaxMissionCount = 2; public const int DefaultMaxMissionCount = 2;
public const int MaxMissionCountLimit = 10; public const int MaxMissionCountLimit = 3;
public const int MinMissionCountLimit = 1; public const int MinMissionCountLimit = 1;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; } public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
@@ -46,15 +46,15 @@ namespace Barotrauma
public static void Init() public static void Init()
{ {
#if CLIENT #if CLIENT
Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true); Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), isSinglePlayer: true);
DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true); DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: true);
SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true); SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), isSinglePlayer: true);
TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true); TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), isSinglePlayer: true);
#endif #endif
Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false); Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: false);
Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false); Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), isSinglePlayer: false);
PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false); PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), isSinglePlayer: false);
MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false); MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), isSinglePlayer: false);
} }
} }
} }

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