Faction Test 100.13.0.0

This commit is contained in:
Markus Isberg
2023-01-11 15:36:23 +02:00
parent 1650735468
commit caa5a2f762
145 changed files with 2100 additions and 1111 deletions
@@ -444,10 +444,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;
} }
} }
@@ -946,7 +946,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;
} }
@@ -83,6 +83,7 @@ namespace Barotrauma
GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox;
Debug.Assert(conversationList != null); Debug.Assert(conversationList != null);
DisableButtons(conversationList.Content.GetAllChildren<GUIButton>(), selectedButton: null);
// gray out the last text block // gray out the last text block
if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement)
{ {
@@ -269,14 +270,7 @@ namespace Barotrauma
if (actionInstance != null) if (actionInstance != null)
{ {
actionInstance.selectedOption = selectedOption; actionInstance.selectedOption = selectedOption;
foreach (GUIButton otherButton in optionButtons) DisableButtons(optionButtons, btn);
{
otherButton.CanBeFocused = false;
if (otherButton != btn)
{
otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f);
}
}
btn.ExternalHighlight = true; btn.ExternalHighlight = true;
return true; return true;
} }
@@ -286,14 +280,7 @@ namespace Barotrauma
SendResponse(actionId.Value, selectedOption); SendResponse(actionId.Value, selectedOption);
btn.CanBeFocused = false; btn.CanBeFocused = false;
btn.ExternalHighlight = true; btn.ExternalHighlight = true;
foreach (GUIButton otherButton in optionButtons) DisableButtons(optionButtons, btn);
{
otherButton.CanBeFocused = false;
if (otherButton != btn)
{
otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f);
}
}
return true; return true;
} }
//should not happen //should not happen
@@ -305,6 +292,18 @@ namespace Barotrauma
} }
} }
public static void SelectOption(ushort actionId, int option)
{
if (lastMessageBox.UserData is Pair<string, ushort> userData)
{
if (userData.Second != actionId) { return; }
GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox;
Debug.Assert(conversationList != null);
DisableButtons(conversationList.Content.GetAllChildren<GUIButton>(), (btn) => btn.UserData is int i && i == option);
}
}
private static Tuple<Vector2, Point> GetSizes(DialogTypes dialogTypes) private static Tuple<Vector2, Point> GetSizes(DialogTypes dialogTypes)
{ {
return dialogTypes switch return dialogTypes switch
@@ -383,6 +382,30 @@ namespace Barotrauma
return buttons; return buttons;
} }
private static void DisableButtons(IEnumerable<GUIButton> buttons, GUIButton selectedButton)
{
DisableButtons(buttons, (btn) => btn == selectedButton);
}
private static void DisableButtons(IEnumerable<GUIButton> buttons, Func<GUIButton, bool> isSelectedButton)
{
foreach (GUIButton btn in buttons)
{
if (btn.CanBeFocused)
{
btn.CanBeFocused = false;
if (isSelectedButton(btn))
{
btn.Selected = true;
}
else
{
btn.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f);
}
}
}
}
private static void SendResponse(UInt16 actionId, int selectedOption) private static void SendResponse(UInt16 actionId, int selectedOption)
{ {
IWriteMessage outmsg = new WriteOnlyMessage(); IWriteMessage outmsg = new WriteOnlyMessage();
@@ -608,47 +608,56 @@ namespace Barotrauma
} }
break; break;
case NetworkEventType.CONVERSATION: case NetworkEventType.CONVERSATION:
UInt16 identifier = msg.ReadUInt16();
string eventSprite = msg.ReadString();
byte dialogType = msg.ReadByte();
bool continueConversation = msg.ReadBoolean();
UInt16 speakerId = msg.ReadUInt16();
string text = msg.ReadString();
bool fadeToBlack = msg.ReadBoolean();
byte optionCount = msg.ReadByte();
List<string> options = new List<string>();
for (int i = 0; i < optionCount; i++)
{ {
options.Add(msg.ReadString()); UInt16 identifier = msg.ReadUInt16();
} string eventSprite = msg.ReadString();
byte dialogType = msg.ReadByte();
byte endCount = msg.ReadByte(); bool continueConversation = msg.ReadBoolean();
int[] endings = new int[endCount]; UInt16 speakerId = msg.ReadUInt16();
for (int i = 0; i < endCount; i++) string text = msg.ReadString();
{ bool fadeToBlack = msg.ReadBoolean();
endings[i] = msg.ReadByte(); byte optionCount = msg.ReadByte();
} List<string> options = new List<string>();
for (int i = 0; i < optionCount; i++)
if (string.IsNullOrEmpty(text) && optionCount == 0)
{
GUIMessageBox.MessageBoxes.ForEachMod(mb =>
{ {
if (mb.UserData is Pair<string, UInt16> pair && pair.First == "ConversationAction" && pair.Second == identifier) options.Add(msg.ReadString());
}
byte endCount = msg.ReadByte();
int[] endings = new int[endCount];
for (int i = 0; i < endCount; i++)
{
endings[i] = msg.ReadByte();
}
if (string.IsNullOrEmpty(text) && optionCount == 0)
{
GUIMessageBox.MessageBoxes.ForEachMod(mb =>
{ {
(mb as GUIMessageBox)?.Close(); if (mb.UserData is Pair<string, UInt16> pair && pair.First == "ConversationAction" && pair.Second == identifier)
} {
}); (mb as GUIMessageBox)?.Close();
}
});
}
else
{
ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation);
}
if (Entity.FindEntityByID(speakerId) is Character speaker)
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.SetCustomInteract(null, null);
}
break;
} }
else case NetworkEventType.CONVERSATION_SELECTED_OPTION:
{ {
ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); UInt16 identifier = msg.ReadUInt16();
int selectedOption = msg.ReadByte() - 1;
ConversationAction.SelectOption(identifier, selectedOption);
break;
} }
if (Entity.FindEntityByID(speakerId) is Character speaker)
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.SetCustomInteract(null, null);
}
break;
case NetworkEventType.MISSION: case NetworkEventType.MISSION:
Identifier missionIdentifier = msg.ReadIdentifier(); Identifier missionIdentifier = msg.ReadIdentifier();
int locationIndex = msg.ReadInt32(); int locationIndex = msg.ReadInt32();
@@ -38,7 +38,7 @@ namespace Barotrauma
return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖"));
} }
public RichString GetReputationRewardText(Location currLocation) public RichString GetReputationRewardText()
{ {
List<LocalizedString> reputationRewardTexts = new List<LocalizedString>(); List<LocalizedString> reputationRewardTexts = new List<LocalizedString>();
foreach (var reputationReward in ReputationRewards) foreach (var reputationReward in ReputationRewards)
@@ -46,7 +46,7 @@ namespace Barotrauma
FactionPrefab targetFaction; FactionPrefab targetFaction;
if (reputationReward.Key == "location" ) if (reputationReward.Key == "location" )
{ {
targetFaction = currLocation.Faction?.Prefab; targetFaction = OriginLocation.Faction?.Prefab;
} }
else else
{ {
@@ -1563,7 +1563,7 @@ namespace Barotrauma
descriptionText += "\n\n" + missionMessage; descriptionText += "\n\n" + missionMessage;
} }
RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub);
RichString reputationText = mission.GetReputationRewardText(mission.Locations[0]); RichString reputationText = mission.GetReputationRewardText();
Func<string, string> wrapMissionText(GUIFont font) Func<string, string> wrapMissionText(GUIFont font)
{ {
@@ -1773,7 +1773,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)
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
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;
@@ -93,6 +92,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;
@@ -605,7 +614,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();
@@ -955,7 +964,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)
{ {
@@ -996,7 +1005,7 @@ namespace Barotrauma
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)
@@ -1086,13 +1095,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, characterList); int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList);
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;
@@ -1110,21 +1129,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)
@@ -1132,12 +1156,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;
@@ -1146,15 +1191,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();
@@ -1180,7 +1223,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)
@@ -1202,31 +1245,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),
@@ -1245,7 +1278,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)
@@ -1628,7 +1661,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)
@@ -1641,36 +1674,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, characterList);
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, characterList);
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
};
} }
} }
} }
@@ -735,8 +735,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 +1099,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")
@@ -85,7 +85,6 @@ namespace Barotrauma
/// </summary> /// </summary>
private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings)
{ {
CampaignMetadata = new CampaignMetadata();
UpgradeManager = new UpgradeManager(this); UpgradeManager = new UpgradeManager(this);
Settings = settings; Settings = settings;
InitFactions(); InitFactions();
@@ -107,18 +106,16 @@ namespace Barotrauma
private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty)
{ {
IsFirstRound = false; IsFirstRound = false;
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
switch (subElement.Name.ToString().ToLowerInvariant()) switch (subElement.Name.ToString().ToLowerInvariant())
{ {
case "metadata": case "metadata":
CampaignMetadata = new CampaignMetadata(subElement); CampaignMetadata.Load(subElement);
break; break;
} }
} }
CampaignMetadata ??= new CampaignMetadata();
InitFactions(); InitFactions();
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
@@ -21,7 +21,7 @@ namespace Barotrauma
private readonly GameMode gameMode; private readonly GameMode gameMode;
private readonly Dictionary<Faction, float> initialFactionReputations = new Dictionary<Faction, float>(); private readonly Dictionary<Identifier, float> initialFactionReputations = new Dictionary<Identifier, float>();
public GUILayoutGroup ButtonArea { get; private set; } public GUILayoutGroup ButtonArea { get; private set; }
@@ -39,7 +39,7 @@ namespace Barotrauma
{ {
foreach (Faction faction in campaignMode.Factions) foreach (Faction faction in campaignMode.Factions)
{ {
initialFactionReputations.Add(faction, faction.Reputation.Value); initialFactionReputations.Add(faction.Prefab.Identifier, faction.Reputation.Value);
} }
} }
} }
@@ -312,17 +312,26 @@ namespace Barotrauma
var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform),
RichString.Rich(missionMessage), wrap: true); RichString.Rich(missionMessage), wrap: true);
int reward = displayedMission.GetReward(Submarine.MainSub); int reward = displayedMission.GetReward(Submarine.MainSub);
if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) if (selectedMissions.Contains(displayedMission) && displayedMission.Completed)
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); RichString reputationText = displayedMission.GetReputationRewardText();
if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) if (!reputationText.IsNullOrEmpty())
{ {
var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option<int>.Some(reward)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText);
if (share > 0) }
if (reward > 0)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub)));
if (GameMain.IsMultiplayer && Character.Controlled is { } controlled)
{ {
string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option<int>.Some(reward));
RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); if (share > 0)
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); {
string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share);
RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}")));
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString);
}
} }
} }
} }
@@ -400,26 +409,10 @@ namespace Barotrauma
}; };
reputationList.ContentBackground.Color = Color.Transparent; reputationList.ContentBackground.Color = Color.Transparent;
/*if (startLocation.Type.HasOutpost && startLocation.Reputation != null)
{
var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon");
var locationFrame = CreateReputationElement(
reputationList.Content,
startLocation.Name,
startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation,
startLocation.Type.Name, "",
iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White);
CreatePathUnlockElement(locationFrame, null, startLocation);
}*/
foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name))
{ {
float initialReputation = faction.Reputation.Value; float initialReputation = faction.Reputation.Value;
if (initialFactionReputations.ContainsKey(faction)) if (!initialFactionReputations.TryGetValue(faction.Prefab.Identifier, out initialReputation))
{
initialReputation = initialFactionReputations[faction];
}
else
{ {
DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round).");
} }
@@ -454,47 +447,57 @@ namespace Barotrauma
void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location) void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location)
{ {
if (GameMain.GameSession?.Campaign?.Map != null) if (GameMain.GameSession?.Campaign?.Map == null) { return; }
IEnumerable<LocationConnection> connectionsBetweenBiomes =
GameMain.GameSession.Campaign.Map.Connections.Where(c => c.Locations[0].Biome != c.Locations[1].Biome);
foreach (LocationConnection connection in connectionsBetweenBiomes)
{ {
foreach (LocationConnection connection in GameMain.GameSession.Campaign.Map.Connections) if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; }
//don't show the "reputation required to unlock" text if another connection between the biomes has already been unlocked
if (connectionsBetweenBiomes.Where(c => !c.Locked).Any(c =>
(c.Locations[0].Biome == connection.Locations[0].Biome && c.Locations[1].Biome == connection.Locations[1].Biome) ||
(c.Locations[1].Biome == connection.Locations[0].Biome && c.Locations[0].Biome == connection.Locations[1].Biome)))
{ {
if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } continue;
}
var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1];
var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction);
if (unlockEvent == null) { continue; } if (unlockEvent == null) { continue; }
if (unlockEvent.Faction.IsEmpty) if (unlockEvent.Faction.IsEmpty)
{
if (location == null || gateLocation != location) { continue; }
}
else
{
if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; }
}
if (unlockEvent != null)
{
Reputation unlockReputation = gateLocation.Reputation;
Faction unlockFaction = null;
if (!unlockEvent.Faction.IsEmpty)
{ {
if (location == null || gateLocation != location) { continue; } unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction);
unlockReputation = unlockFaction?.Reputation;
} }
else float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation);
RichString unlockText = RichString.Rich(TextManager.GetWithVariables(
"lockedpathreputationrequirement",
("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)),
("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖")));
var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) },
unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal);
unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f);
unlockInfoPanel.UserData = "unlockinfo";
if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f)
{ {
if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } unlockInfoPanel.Font = GUIStyle.SmallFont;
}
if (unlockEvent != null)
{
Reputation unlockReputation = gateLocation.Reputation;
Faction unlockFaction = null;
if (!unlockEvent.Faction.IsEmpty)
{
unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction);
unlockReputation = unlockFaction?.Reputation;
}
float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation);
RichString unlockText = RichString.Rich(TextManager.GetWithVariables(
"lockedpathreputationrequirement",
("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)),
("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖")));
var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) },
unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal);
unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f);
unlockInfoPanel.UserData = "unlockinfo";
if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f)
{
unlockInfoPanel.Font = GUIStyle.SmallFont;
}
} }
} }
} }
@@ -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);
} }
@@ -1181,13 +1181,18 @@ namespace Barotrauma.Items.Components
if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine == null) { continue; }
if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; }
// docking ports should be shown even if defined as not, if the submarine is the same as the sonar's // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's
if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine &&
!dockingPort.Item.Submarine.Info.IsOutpost && !dockingPort.Item.Submarine.Info.IsBeacon)
{
continue;
}
//don't show the docking ports of the opposing team on the sonar //don't show the docking ports of the opposing team on the sonar
if (item.Submarine != null && if (item.Submarine != null &&
item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) !dockingPort.Item.Submarine.Info.IsOutpost &&
!dockingPort.Item.Submarine.Info.IsBeacon)
{ {
// specifically checking for friendlyNPC seems more logical here // specifically checking for friendlyNPC seems more logical here
if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; } if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; }
@@ -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,7 +260,6 @@ 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())
{ {
@@ -268,8 +267,9 @@ namespace Barotrauma
return; return;
} }
var potentialContainer = MapEntity.GetPotentialContainer(position); var potentialContainer = MapEntity.GetPotentialContainer(cam.ScreenToWorld(PlayerInput.MousePosition));
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)
{ {
@@ -544,9 +544,6 @@ namespace Barotrauma
Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y);
Vector2 viewOffset = DrawOffset + drawOffsetNoise; Vector2 viewOffset = DrawOffset + drawOffsetNoise;
bool cursorOnOverlay = false;
if (HighlightedLocation != null) if (HighlightedLocation != null)
{ {
Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom;
@@ -574,53 +571,42 @@ namespace Barotrauma
{ {
locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight; locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight;
} }
Rectangle highlightedLocationRect = new Rectangle(highlightedLocationDrawPos.ToPoint(), new Point(GUI.IntScale(25)));
Rectangle overlayRect = Rectangle.Union(highlightedLocationRect, locationInfoRt.Rect);
if (overlayRect.Contains(PlayerInput.MousePosition))
{
cursorOnOverlay = true;
}
locationInfoOverlay?.AddToGUIUpdateList(order: 1); locationInfoOverlay?.AddToGUIUpdateList(order: 1);
} }
if (!cursorOnOverlay) float closestDist = 0.0f;
HighlightedLocation = null;
if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer))
{ {
float closestDist = 0.0f; for (int i = 0; i < Locations.Count; i++)
HighlightedLocation = null;
if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer))
{ {
for (int i = 0; i < Locations.Count; i++) Location location = Locations[i];
if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; }
Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom;
if (!rect.Contains(pos)) { continue; }
Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite;
float iconScale = generationParams.LocationIconSize / locationSprite.size.X;
if (location == currentDisplayLocation) { iconScale *= 1.2f; }
Rectangle drawRect = locationSprite.SourceRect;
drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f);
drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f);
drawRect.X = (int)pos.X - drawRect.Width / 2;
drawRect.Y = (int)pos.Y - drawRect.Width / 2;
if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; }
float dist = Vector2.Distance(PlayerInput.MousePosition, pos);
if (HighlightedLocation == null || dist < closestDist)
{ {
Location location = Locations[i]; closestDist = dist;
if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } HighlightedLocation = location;
Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom;
if (!rect.Contains(pos)) { continue; }
Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite;
float iconScale = generationParams.LocationIconSize / locationSprite.size.X;
if (location == currentDisplayLocation) { iconScale *= 1.2f; }
Rectangle drawRect = locationSprite.SourceRect;
drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f);
drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f);
drawRect.X = (int)pos.X - drawRect.Width / 2;
drawRect.Y = (int)pos.Y - drawRect.Width / 2;
if (!drawRect.Contains(PlayerInput.MousePosition)) { continue; }
float dist = Vector2.Distance(PlayerInput.MousePosition, pos);
if (HighlightedLocation == null || dist < closestDist)
{
closestDist = dist;
HighlightedLocation = location;
}
} }
} }
} }
if (SelectedConnection != null) if (SelectedConnection != null)
{ {
connectionHighlightState = Math.Min(connectionHighlightState + deltaTime, 1.0f); connectionHighlightState = Math.Min(connectionHighlightState + deltaTime, 1.0f);
@@ -528,11 +528,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)
@@ -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;
} }
@@ -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,36 +1588,25 @@ 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();
@@ -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;
} }
@@ -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)
@@ -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));
@@ -474,7 +474,7 @@ namespace Barotrauma
}; };
missionRewardTexts.Add(rewardText); missionRewardTexts.Add(rewardText);
LocalizedString reputationText = mission.GetReputationRewardText(mission.Locations[0]); LocalizedString reputationText = mission.GetReputationRewardText();
if (!reputationText.IsNullOrEmpty()) if (!reputationText.IsNullOrEmpty())
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true);
@@ -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() +
@@ -900,18 +916,9 @@ 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)
{ {
@@ -106,7 +106,7 @@ namespace Barotrauma
{ {
var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)];
currentText = slide.Text currentText = slide.Text
.Replace("[submarine]", Submarine.MainSub?.Info.Name ?? "Unknown") .Replace("[submarine]", Submarine.MainSub?.Info.Name ?? GameMain.GameSession?.SubmarineInfo?.Name ?? "Unknown")
.Replace("[location]", Level.Loaded?.StartOutpost?.Info.Name ?? "Unknown"); .Replace("[location]", Level.Loaded?.StartOutpost?.Info.Name ?? "Unknown");
} }
@@ -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)
@@ -5646,8 +5634,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()
@@ -904,17 +904,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;
@@ -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>100.11.0.0</Version> <Version>100.13.0.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>100.11.0.0</Version> <Version>100.13.0.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>100.11.0.0</Version> <Version>100.13.0.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>100.11.0.0</Version> <Version>100.13.0.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>100.11.0.0</Version> <Version>100.13.0.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>
@@ -302,12 +302,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 +401,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)
@@ -41,7 +41,7 @@ namespace Barotrauma
clientsToRemove.Add(k); clientsToRemove.Add(k);
} }
} }
if (!(clientsToRemove is null)) if (clientsToRemove is not null)
{ {
foreach (var k in clientsToRemove) foreach (var k in clientsToRemove)
{ {
@@ -62,7 +62,7 @@ namespace Barotrauma
{ {
foreach (Entity e in targets) foreach (Entity e in targets)
{ {
if (!(e is Character character) || !character.IsRemotePlayer) { continue; } if (e is not Character character || !character.IsRemotePlayer) { continue; }
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (targetClient != null) if (targetClient != null)
{ {
@@ -85,7 +85,7 @@ namespace Barotrauma
IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag); IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag);
foreach (Entity e in entities) foreach (Entity e in entities)
{ {
if (!(e is Character character) || !character.IsRemotePlayer) { continue; } if (e is not Character character || !character.IsRemotePlayer) { continue; }
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (targetClient != null) if (targetClient != null)
{ {
@@ -149,5 +149,15 @@ namespace Barotrauma
} }
GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
} }
public void ServerWriteSelectedOption(Client client)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
outmsg.WriteByte((byte)EventManager.NetworkEventType.CONVERSATION_SELECTED_OPTION);
outmsg.WriteUInt16(Identifier);
outmsg.WriteByte((byte)(selectedOption + 1));
GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
} }
} }
@@ -14,12 +14,12 @@ namespace Barotrauma
foreach (Event ev in activeEvents) foreach (Event ev in activeEvents)
{ {
if (!(ev is ScriptedEvent scriptedEvent)) { continue; } if (ev is not ScriptedEvent scriptedEvent) { continue; }
var actions = FindActions(scriptedEvent); var actions = FindActions(scriptedEvent);
foreach (EventAction action in actions.Select(a => a.Item2)) foreach (EventAction action in actions.Select(a => a.Item2))
{ {
if (!(action is ConversationAction convAction) || convAction.Identifier != actionId) { continue; } if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; }
if (!convAction.TargetClients.Contains(sender)) if (!convAction.TargetClients.Contains(sender))
{ {
#if DEBUG || UNSTABLE #if DEBUG || UNSTABLE
@@ -42,6 +42,14 @@ namespace Barotrauma
else else
{ {
convAction.SelectedOption = selectedOption; convAction.SelectedOption = selectedOption;
if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption))
{
foreach (Client c in convAction.TargetClients)
{
if (c == sender) { continue; }
convAction.ServerWriteSelectedOption(c);
}
}
} }
} }
return; return;
@@ -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)
@@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components
msg.WriteBoolean(launch); msg.WriteBoolean(launch);
if (launch) if (launch)
{ {
msg.WriteUInt16(User?.ID ?? 0); 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);
@@ -349,15 +349,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
@@ -379,7 +373,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)
@@ -1745,9 +1745,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 +1758,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();
@@ -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>100.11.0.0</Version> <Version>100.13.0.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>
@@ -30,6 +30,7 @@
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>TRACE;SERVER;WINDOWS;USE_STEAM</DefineConstants> <DefineConstants>TRACE;SERVER;WINDOWS;USE_STEAM</DefineConstants>
<PlatformTarget>x64</PlatformTarget> <PlatformTarget>x64</PlatformTarget>
@@ -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>
@@ -352,6 +352,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)
@@ -354,6 +354,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;
@@ -364,7 +368,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)
{ {
@@ -689,12 +693,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.IsPet || other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.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)
@@ -83,6 +83,7 @@ namespace Barotrauma
{ {
if (GameMain.GameSession.RoundDuration < 120.0f && if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null && speaker?.CurrentHull != null &&
GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{ {
@@ -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++;
} }
@@ -118,7 +118,11 @@ namespace Barotrauma
Abandon = true; Abandon = true;
return; return;
} }
ItemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true); ItemToContain = item ?? character.Inventory.FindItem(it =>
CheckItem(it) &&
//ignore items already in the container, unless we're trying to place to a specific slot, and the item's not in it
(it.Container != container.Item || (TargetSlot.HasValue && it.Container.OwnInventory.FindIndex(it) != TargetSlot)),
recursive: true);
if (ItemToContain != null) if (ItemToContain != null)
{ {
if (!character.CanInteractWith(ItemToContain, checkLinked: false)) if (!character.CanInteractWith(ItemToContain, checkLinked: false))
@@ -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
@@ -412,7 +412,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));
} }
@@ -422,7 +423,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)
{ {
@@ -1043,7 +1043,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)
{ {
@@ -431,7 +431,7 @@ namespace Barotrauma
} }
} }
if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck) if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck)
{ {
Flip(); Flip();
} }
@@ -1723,7 +1723,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 +1822,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
@@ -881,7 +885,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();
@@ -1436,6 +1440,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);
@@ -498,37 +498,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)
@@ -538,18 +553,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);
} }
} }
@@ -585,47 +612,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);
} }
} }
@@ -365,9 +365,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 => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.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;
@@ -1613,7 +1619,7 @@ namespace Barotrauma
{ {
DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\"."); DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\".");
} }
else if (humanPrefab.GiveItems(this, Submarine, spawnPoint)) else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
{ {
return; return;
} }
@@ -1752,7 +1758,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();
@@ -3881,8 +3887,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);
@@ -4481,7 +4487,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();
@@ -5197,15 +5206,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 || CharacterParams.CompareGroup(me.Group, other.Group);
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
public void StopClimbing() public void StopClimbing()
{ {
@@ -779,9 +779,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);
@@ -146,12 +146,6 @@ namespace Barotrauma
{ {
return minVitality; return minVitality;
} }
if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
return Math.Max(vitality, MinVitality + 1);
}
return vitality; return vitality;
} }
@@ -587,6 +581,15 @@ namespace Barotrauma
} }
} }
private void KillIfOutOfVitality()
{
if (Vitality <= MinVitality &&
!Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
Kill();
}
}
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>(); private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>(); private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
@@ -611,7 +614,7 @@ namespace Barotrauma
} }
CalculateVitality(); CalculateVitality();
if (Vitality <= MinVitality) { Kill(); } KillIfOutOfVitality();
} }
public float GetLimbDamage(Limb limb, string afflictionType = null) public float GetLimbDamage(Limb limb, string afflictionType = null)
@@ -729,10 +732,7 @@ namespace Barotrauma
existingAffliction.Duration = existingAffliction.Prefab.Duration; existingAffliction.Duration = existingAffliction.Prefab.Duration;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality(); CalculateVitality();
if (Vitality <= MinVitality) KillIfOutOfVitality();
{
Kill();
}
return; return;
} }
@@ -746,10 +746,7 @@ namespace Barotrauma
Character.HealthUpdateInterval = 0.0f; Character.HealthUpdateInterval = 0.0f;
CalculateVitality(); CalculateVitality();
if (Vitality <= MinVitality) KillIfOutOfVitality();
{
Kill();
}
#if CLIENT #if CLIENT
if (OpenHealthWindow != this && limbHealth != null) if (OpenHealthWindow != this && limbHealth != null)
{ {
@@ -844,11 +841,7 @@ namespace Barotrauma
} }
#endif #endif
CalculateVitality(); CalculateVitality();
KillIfOutOfVitality();
if (Vitality <= MinVitality)
{
Kill();
}
} }
} }
@@ -879,7 +872,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;
@@ -1025,17 +1022,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)
{ {
@@ -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:
@@ -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,5 +1,6 @@
#nullable enable #nullable enable
using System;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
@@ -17,6 +18,9 @@ namespace Barotrauma.Abilities
{ {
if (!addingFirstTime) { return; } if (!addingFirstTime) { return; }
// do not run client-side in multiplayer
if (GameMain.NetworkMember is { IsClient: true }) { return; }
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
if (apprentice is null) if (apprentice is null)
{ {
@@ -49,6 +49,16 @@ namespace Barotrauma.Abilities
break; break;
} }
} }
switch (abilityEffectType)
{
case AbilityEffectType.OnDieToCharacter:
if (characterAbilities.Any(a => a.RequiresAlive))
{
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.");
}
break;
}
} }
public void ActivateAbilityGroup(bool addingFirstTime) public void ActivateAbilityGroup(bool addingFirstTime)
@@ -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;
@@ -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();
@@ -1263,6 +1263,22 @@ namespace Barotrauma
} }
#endif #endif
commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) =>
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
NewMessage("Reputation:");
foreach (var faction in campaign.Factions)
{
NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}");
}
}
else
{
ThrowError("Could not show reputation (no active campaign).");
}
}, null));
commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) => commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) =>
{ {
if (GameMain.GameSession?.GameMode is CampaignMode campaign) if (GameMain.GameSession?.GameMode is CampaignMode campaign)
@@ -1317,8 +1333,8 @@ 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));
@@ -171,7 +171,7 @@ namespace Barotrauma
PumpSpeed, PumpSpeed,
PumpMaxFlow, PumpMaxFlow,
ReactorMaxOutput, ReactorMaxOutput,
ReactorFuelEfficiency, ReactorFuelConsumption,
DeconstructorSpeed, DeconstructorSpeed,
FabricationSpeed FabricationSpeed
} }
@@ -200,7 +200,7 @@ namespace Barotrauma
} }
} }
private int[] GetEndingOptions() public int[] GetEndingOptions()
{ {
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList(); List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
if (!ContinueConversation) { endings.Add(-1); } if (!ContinueConversation) { endings.Add(-1); }
@@ -68,7 +68,7 @@ namespace Barotrauma
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable()); var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable());
if (emptyLocation != null) if (emptyLocation != null)
{ {
emptyLocation.ChangeType(campaign, Barotrauma.LocationType.Prefabs[LocationTypes[0]]); emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation; unlockLocation = emptyLocation;
} }
} }
@@ -89,8 +89,9 @@ namespace Barotrauma
} }
if (unlockedMission != null) if (unlockedMission != null)
{ {
unlockedMission.OriginLocation = campaign.Map.CurrentLocation;
campaign.Map.Discover(unlockLocation, checkTalents: false); campaign.Map.Discover(unlockLocation, checkTalents: false);
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null) if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null)
{ {
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\"."); DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\".");
} }
@@ -12,6 +12,7 @@ namespace Barotrauma
public enum NetworkEventType public enum NetworkEventType
{ {
CONVERSATION, CONVERSATION,
CONVERSATION_SELECTED_OPTION,
STATUSEFFECT, STATUSEFFECT,
MISSION, MISSION,
UNLOCKPATH UNLOCKPATH
@@ -229,9 +229,8 @@ namespace Barotrauma
} }
bool requiresRescue = element.GetAttributeBool("requirerescue", false); bool requiresRescue = element.GetAttributeBool("requirerescue", false);
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos, giveTags: true); Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{ {
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier); outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
@@ -240,6 +239,7 @@ namespace Barotrauma
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag); outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
} }
} }
if (spawnPos is WayPoint wp) if (spawnPos is WayPoint wp)
{ {
spawnedCharacter.GiveIdCardTags(wp); spawnedCharacter.GiveIdCardTags(wp);
@@ -94,7 +94,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 ||
@@ -125,6 +125,12 @@ namespace Barotrauma
public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier; public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier;
/// <summary>
/// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission.
/// Defaults to Locations[0]
/// </summary>
public Location OriginLocation;
public readonly Location[] Locations; public readonly Location[] Locations;
public int? Difficulty public int? Difficulty
@@ -144,7 +150,7 @@ namespace Barotrauma
} }
} }
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>(); private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Action<Mission> OnMissionStateChanged; public Action<Mission> OnMissionStateChanged;
@@ -160,12 +166,13 @@ namespace Barotrauma
Headers = prefab.Headers; Headers = prefab.Headers;
var messages = prefab.Messages.ToArray(); var messages = prefab.Messages.ToArray();
OriginLocation = locations[0];
Locations = locations; Locations = locations;
var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction)); var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction));
if (endConditionElement != null) if (endConditionElement != null)
{ {
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})"); completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})");
} }
for (int n = 0; n < 2; n++) for (int n = 0; n < 2; n++)
@@ -407,7 +414,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
@@ -436,7 +443,7 @@ namespace Barotrauma
{ {
if (reputationReward.Key == "location") if (reputationReward.Key == "location")
{ {
Locations[0].Reputation?.AddReputation(reputationReward.Value); OriginLocation.Reputation?.AddReputation(reputationReward.Value);
} }
else else
{ {
@@ -546,17 +553,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;
} }
@@ -290,6 +290,16 @@ namespace Barotrauma
else if (!character.Info.StartItemsGiven) else if (!character.Info.StartItemsGiven)
{ {
character.GiveJobItems(mainSubWaypoints[i]); character.GiveJobItems(mainSubWaypoints[i]);
foreach (Item item in character.Inventory.AllItems)
{
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.SubmarineSpecificID = 0;
}
}
} }
if (character.Info.HealthData != null) if (character.Info.HealthData != null)
{ {
@@ -298,6 +308,7 @@ namespace Barotrauma
character.LoadTalents(); character.LoadTalents();
character.GiveIdCardTags(mainSubWaypoints[i]);
character.GiveIdCardTags(spawnWaypoints[i]); character.GiveIdCardTags(spawnWaypoints[i]);
character.Info.StartItemsGiven = true; character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null) if (character.Info.OrderData != null)
@@ -14,8 +14,9 @@ namespace Barotrauma
{ {
} }
public CampaignMetadata(XElement element) public void Load(XElement element)
{ {
data.Clear();
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase))
@@ -27,7 +27,7 @@ namespace Barotrauma
/// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static FactionAffiliation GetPlayerAffiliationStatus(Identifier identifier, ImmutableHashSet<Character>? characterList = null) public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet<Character>? characterList = null)
{ {
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; } if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; }
@@ -37,23 +37,20 @@ namespace Barotrauma
{ {
if (character.Info is not { } info) { continue; } if (character.Info is not { } info) { continue; }
foreach (Faction faction in factions) foreach (Faction otherFaction in factions)
{ {
Identifier factionIdentifier = faction.Prefab.Identifier; Identifier factionIdentifier = otherFaction.Prefab.Identifier;
if (info.GetSavedStatValue(StatTypes.Affiliation, factionIdentifier) > 0f) if (info.GetSavedStatValue(StatTypes.Affiliation, factionIdentifier) > 0f)
{ {
return factionIdentifier == identifier return factionIdentifier == faction.Prefab.Identifier
? FactionAffiliation.Positive ? FactionAffiliation.Positive
: FactionAffiliation.Negative; : FactionAffiliation.Negative;
} }
} }
} }
return FactionAffiliation.Neutral; return FactionAffiliation.Neutral;
} }
public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet<Character>? characterList = null) => GetPlayerAffiliationStatus(faction.Prefab.Identifier, characterList);
public override string ToString() public override string ToString()
{ {
return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})"; return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})";
@@ -46,7 +46,7 @@ namespace Barotrauma
private List<Faction> factions; private List<Faction> factions;
public IReadOnlyList<Faction> Factions => factions; public IReadOnlyList<Faction> Factions => factions;
public CampaignMetadata CampaignMetadata; public readonly CampaignMetadata CampaignMetadata;
protected XElement petsElement; protected XElement petsElement;
@@ -157,6 +157,7 @@ namespace Barotrauma
CargoManager = new CargoManager(this); CargoManager = new CargoManager(this);
MedicalClinic = new MedicalClinic(this); MedicalClinic = new MedicalClinic(this);
CampaignMetadata = new CampaignMetadata();
Identifier messageIdentifier = new Identifier("money"); Identifier messageIdentifier = new Identifier("money");
#if CLIENT #if CLIENT
@@ -675,9 +676,11 @@ namespace Barotrauma
//TODO: ignore players who don't have the permission to trigger a transition between levels? //TODO: ignore players who don't have the permission to trigger a transition between levels?
var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer)); var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer));
CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1;
//allow leaving if inside an outpost, and the submarine is either docked to it or close enough //allow leaving if inside an outpost, and the submarine is either docked to it or close enough
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers); Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam);
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers); Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam);
int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 : int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 :
leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost)); leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
@@ -691,11 +694,11 @@ namespace Barotrauma
return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd; return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers) static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
{ {
if (Level.Loaded.StartOutpost == null) if (Level.Loaded.StartOutpost == null)
{ {
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null) { return null; } if (closestSub == null) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
} }
@@ -705,23 +708,23 @@ namespace Barotrauma
if (Level.Loaded.StartOutpost.DockedTo.Any()) if (Level.Loaded.StartOutpost.DockedTo.Any())
{ {
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
} }
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; } if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null || !closestSub.AtStartExit) { return null; } if (closestSub == null || !closestSub.AtStartExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
} }
} }
static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers) static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
{ {
if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any()) if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any())
{ {
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null || !closestSub.AtEndExit) { return null; } if (closestSub == null || !closestSub.AtEndExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
} }
@@ -733,7 +736,7 @@ namespace Barotrauma
if (Level.Loaded.EndOutpost == null) if (Level.Loaded.EndOutpost == null)
{ {
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null) { return null; } if (closestSub == null) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
} }
@@ -743,13 +746,13 @@ namespace Barotrauma
if (Level.Loaded.EndOutpost.DockedTo.Any()) if (Level.Loaded.EndOutpost.DockedTo.Any())
{ {
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
} }
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; } if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null || !closestSub.AtEndExit) { return null; } if (closestSub == null || !closestSub.AtEndExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
} }
@@ -870,7 +873,7 @@ namespace Barotrauma
} }
foreach (Location location in Map.Locations) foreach (Location location in Map.Locations)
{ {
location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty);
location.Reset(this); location.Reset(this);
} }
Map.ClearLocationHistory(); Map.ClearLocationHistory();
@@ -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; }
@@ -1,4 +1,5 @@
using Barotrauma.IO; using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
@@ -53,7 +54,7 @@ namespace Barotrauma
} }
} }
private bool ValidateFlag(NetFlags flag) private static bool ValidateFlag(NetFlags flag)
{ {
if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } if (MathHelper.IsPowerOfTwo((int)flag)) { return true; }
#if DEBUG #if DEBUG
@@ -105,7 +106,6 @@ namespace Barotrauma
#endif #endif
} }
CampaignID = currentCampaignID; CampaignID = currentCampaignID;
CampaignMetadata = new CampaignMetadata();
UpgradeManager = new UpgradeManager(this); UpgradeManager = new UpgradeManager(this);
InitFactions(); InitFactions();
} }
@@ -194,7 +194,7 @@ namespace Barotrauma
} }
break; break;
case "metadata": case "metadata":
CampaignMetadata = new CampaignMetadata(subElement); CampaignMetadata.Load(subElement);
break; break;
case "upgrademanager": case "upgrademanager":
case "pendingupgrades": case "pendingupgrades":
@@ -237,10 +237,8 @@ namespace Barotrauma
}; };
} }
CampaignMetadata ??= new CampaignMetadata();
UpgradeManager ??= new UpgradeManager(this); UpgradeManager ??= new UpgradeManager(this);
InitFactions();
#if SERVER #if SERVER
characterData.Clear(); characterData.Clear();
string characterDataPath = GetCharacterDataSavePath(); string characterDataPath = GetCharacterDataSavePath();

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